您的位置:首页 > 其它

实验1正篇——引导PC

2015-10-09 21:20 676 查看
      讲了这么久,终于进入主题了。为了让读者更深入的理解整个系统,做一些必要的铺垫是必需的;这样不仅能做到有理有据,更能让人知其然,而且知其所以然。题目所谓“正”篇,就是要从正面去描述该实验的内容,当然是对其进行意译,同时根据其内容,结合源码,提出自己的理解,相当于为实验1的内容进行翻译的基础上,进行注解。

实验1的主要内容是“引导PC”,主要讲述的是从PC一开机到运行内核的一个过程。实验内容大致分为如下几部分:1.PC引导(Bootstrap),2.内核引导程序(Boot loader),3.内核初识(kernel).当然这是以开机流程的时序为主线来分割内容。同时这也需要我们去理解Bootstrap,boot loader,boot等概念,这些都有引导的意思,但是它们是工作在不同的时段,而且任务也不一样。Bootstrap为从硬件开机引导整个硬件系统,存在于硬件可以执行代码的地方,它们主要有bios,uboot等,boot loader,存在于二级存储器上,用于加载内核的功能,比如:grub;boot,为内核的启动过程,存在于内核的代码中。

参考网址如下:https://pdos.csail.mit.edu/6.828/2012/labs/lab1/

0.实验前的准备

      a)创建实验环境:

      (1)编译交叉编译工具——i486-yiye-linux-gcc

      (2)编译虚拟机——qemu-system-i386

      (3)设置i486-yiye-linux-gcc与qemu-system-i386的路径到环境变量PATH中

       b)下载实验1的源代码:

      因为课程源码是用git来管理,所以需要用git的相关命令来下载源码:

(1)克隆代码到本地:

git clone http://pdos.csail.mit.edu/6.828/2012/jos.git lab1.demo

       (2)浏览代码所在的目录详情

├── boot #实现mbr的代码目录

│   ├── boot.S #mbr进入点的代码

│   ├── main.c #被boot.S调用,加载内核的代码

│   ├── Makefrag#编译boot下的makefile的部分

│   └── sign.pl #修改编译生成的扇区,最后两个字节为0xaa55,生成mbr。

├── CODING 

├── conf #编译的配置目录

│   ├── env.mk #编译环境的配置——交叉编译工具与虚拟机

│   └── lab.mk #实现课程的配置

├── GNUmakefile #编译整个系统的makefile,主要生成boot.img

├── grade-lab1

├── gradelib.py

├── handin-prep

├── inc  #主要头文件,用于应用引用或者内核引用

│   ├── assert.h#c语言中的assert的实现

│   ├── COPYRIGHT

│   ├── elf.h #elf32的文件格式定义

│   ├── error.h #错误的处理

│   ├── kbdreg.h#按键的寄存器配置

│   ├── memlayout.h#内核管理内存映像的配置

│   ├── mmu.h #内存mmu的管理

│   ├── stab.h #elf文件的slab调试信息

│   ├── stdarg.h#支持c语言变参数的定义

│   ├── stdio.h #标准io的头文件实现

│   ├── string.h#字符串的头文件实现

│   ├── types.h #标准的类型定义

│   └── x86.h #x86的相关内敛函数的实现

├── kern #内核的相关代码实现,其中的头文件用于内核的引用

│   ├── console.c#终端的输入输出实现

│   ├── console.h#终端的头文件

│   ├── COPYRIGHT

│   ├── entrypgdir.c#保护模式下的页表进入点

│   ├── entry.S #内核的进入点代码

│   ├── init.c #内核的初始化代码

│   ├── kdebug.c#内核调试代码实现

│   ├── kdebug.h#调试的头文件

│   ├── kernel.ld#内核链接脚本

│   ├── Makefrag#内核编译的makefile

│   ├── monitor.c#内核模拟终端实现

│   ├── monitor.h#相应头文件

│   └── printf.c#终端标准输出实现

├── lib  #内核引用库的代码实现

│   ├── printfmt.c#格式化标准输出

│   ├── readline.c#从终端读取行数据

│   └── string.c#字符串的处理实现

└── mergedep.pl

(3)修改编译环境-env.mk:

 GCCPREFIX='i486-yiye-linux-'

 QEMU=qemu-system-i386

(4)编译——make 



(5)运行——make qemu或者手动运行之



1.PC引导(PC Bootstrap)

        这部分的主要内容是x86的汇编语言,pc引导流程,以及用qemu模拟pc与qemu配合gdb调试整个流程。这个部分的学习,需要能够去理解相关的内容。

        a)熟悉x86平台上的汇编实现——这里只需要提醒linux下使用的汇编为AT&T而Intel汇编可以由工具NASM来进行编译。详细的部分可以参考我之前的博客相关内容。如下为原文的参考文献:

       介绍pc的汇编:https://pdos.csail.mit.edu/6.828/2012/readings/pcasm-book.pdf

        c语言内嵌汇编的介绍:http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html

        intel的开发手册:http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

        b)仿真x86——我们使用现成的工具qemu,熟悉它请参考之前的博客。

        c)PC开机的映像:



          关于低1M的内存空间详细使用可以参考《BIOS编程空间》的BIOS的数据资料。

          因为早期的PC是在8088处理器的基础上构架的,所以目前系统为了兼容之前的PC系统,开始时处于8088的状态,访问内存空间只有1M。在内存范围0x00000-0xA0000标记为“低内存”(640KByte),这部分内存只能被编程作为RAM来使用。内存范围0xA0000-0xFFFFF的384KByte部分为显示区域与BIOS的程序部分,这部分空间被放在非易失性存储器。最重要的部分为BIOS代码部分——0xF0000-0xFFFFF。BIOS的基本功能是执行系统的初始化,比如:激活视频卡,查询系统存在的内存等。当系统初始化完了,BIOS将会加载二级存储空间(软盘,CD-ROM,硬盘,网络等)到内存中,从而引导操作系统。最后由操作系统去初始化所有的内存空间,管理所有的硬件。

         d)BOIS的调试(The ROM BIOS)

         PC一上电,处理器POST之后,它就会去0xffff0拿指令执行(刚好该地址在BIOS的地址空间中),然后执行BIOS的代码。为此我们用 make qemu-gdb进行调试。有如下输出:

         qemu-system-i386 -hda obj/kern/kernel.img -serial mon:stdio -gdb tcp::26000 -D qemu.log  -S

        将编译出来的kernel.img作为硬盘1,添加串口stdio,调试端口为tcp::26000,停止处理器-S。更详细的内容查看qemu的使用。

        当程序停止后,我们需要重新启动一个终端去启动gdb,然后远程去调试我们刚运行的模拟器。执行如下命令:

gdb -q -iex 'add-auto-load-safe-path .' .gdbinit

然后得到如下打印:



如上的内容被.gdbinit所指定。

通过查看当前寄存器的状态:



        可以发现当前指令为[0xf000:0xfff0]内存0xffff0来实现,从而发现了BIOS的进入点。 

        通过反汇编 x/i $cs*16+$eip发现当前的指令是 ljmp   $0xf000,$0xe05b,即调转到0xfe05b执行代码。然后调试步骤,需要耐心与其他硬件的知识了。通过si或者ni单条指令执行来调试;因为我们的研究重点是操作系统,而不是BIOS,所以只需要了解它的基本工作原理,当然也可以去详细的反汇编它们且一步步调试它。另外需要了解的目前的内存访问方式为physical address = 16 * segment + offset.。

比如:

(gdb) si

[f000:e05b]    0xfe05b:cmpl   $0x0,%cs:0x6bb8

0x0000e05b in ?? ()

       更详细的内容可以参考查阅Intel公司的x86资料<<64-ia-32-architectures-software-developer-manual-325462.pdf>>第三卷-第九章

       根据表9-1.

初始化的状态:控制寄存器CR0=0x6000,0010(0110,0000,0000,0000,0000,0000,0001,0000)2.根据第三卷-2.5节知道系统初始状态处于

CR0.PE(bit0)=0,保护模式没有开启,所以系统处于8086的模式

CR0.PG(bit31)=0,分页机制被禁用,系统访问内存的方式为平坦模式,通过地址直接得到内存值。

CR0.CD(bit30)=1,处理器内部的缓存被禁用

CR0.NW(bit29)=1,处理器的回写功能被禁用

CR0.AM(bit18)=0,自动对齐检测关闭

CR0.WP(bit16)=0,写保护,被操作系统使用

CR0.NE(bit5)=1,检测协处理器X87的算术错误

CR0.AT(bit4)=0,扩展类型

CR0.EM/MP/TS(bit1,2,3)=0,跟协处理器的任务调度相关

2.内核引导程序(Boot Loader)
       当BIOS执行完成之后,它会将硬盘的第一扇区MBR加载内存0x7C000中执行,而MBR的代码是由目录下boot实现的,boot下主要有boot.S与main.c。它们的主要功能如下:

a.boot.S将系统从16位模式切换到32位模式,然后调用bootmain(main.c的入口)

.globl start
start:
.code16                     # Assemble for 16-bit mode
cli                         # Disable interrupts
cld                         # String operations increment

# Set up the important data segment registers (DS, ES, SS).
xorw    %ax,%ax             # Segment number zero
movw    %ax,%ds             # -> Data Segment
movw    %ax,%es             # -> Extra Segment
movw    %ax,%ss             # -> Stack Segment

# Enable A20:
#   For backwards compatibility with the earliest PCs, physical
#   address line 20 is tied low, so that addresses higher than
#   1MB wrap around to zero by default.  This code undoes this.
seta20.1:
inb     $0x64,%al               # Wait for not busy
testb   $0x2,%al
jnz     seta20.1

movb    $0xd1,%al               # 0xd1 -> port 0x64
outb    %al,$0x64

seta20.2:
inb     $0x64,%al               # Wait for not busy
testb   $0x2,%al
jnz     seta20.2

movb    $0xdf,%al               # 0xdf -> port 0x60
outb    %al,$0x60

# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to their physical addresses, so that the
# effective memory map does not change during the switch.
lgdt    gdtdesc
movl    %cr0, %eax
orl     $CR0_PE_ON, %eax
movl    %eax, %cr0

# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp    $PROT_MODE_CSEG, $protcseg

.code32                     # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw    $PROT_MODE_DSEG, %ax    # Our data segment selector
movw    %ax, %ds                # -> DS: Data Segment
movw    %ax, %es                # -> ES: Extra Segment
movw    %ax, %fs                # -> FS
movw    %ax, %gs                # -> GS
movw    %ax, %ss                # -> SS: Stack Segment

# Set up the stack pointer and call into C.
movl    $start, %esp
call bootmain

# If bootmain returns (it shouldn't), loop.
spin:
jmp spin

# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
gdt:
SEG_NULL				# null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff)	# code seg
SEG(STA_W, 0x0, 0xffffffff)	        # data seg

gdtdesc:
.word   0x17                            # sizeof(gdt) - 1
.long   gdt                             # address gdt</span></em>


        b.main.c用于加载内核代码到内存中,然后执行它。

<blockquote style="margin-right: 0px;" dir="ltr"><pre class="plain" name="code">void
bootmain(void)
{
struct Proghdr *ph, *eph;

// read 1st page off disk
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;

// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))();

bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}



       为了将如上的步骤完整的演示,可以通过直接反汇编出来boot.asm(objdump -d -S 即可)。
       而要通过gdb去追踪该过程只需要在地址0x7c00设置断点——b *0x7C00,执行如下:

(gdb) b *0x7c00

Breakpoint 1 at 0x7c00

(gdb) c

Continuing.

[   0:7c00] => 0x7c00:cli    

当了解执行流程之后,我们还需要更具体的知道代码的实现,首先boot.S检查了A20,关于A20的详细介绍,请参考:http://www.win.tue.nl/~aeb/linux/kbd/A20.html
       为了访问更多的内存,将A20使能。然后切换模式到32位保护模式,用于访问更多的内存空间。

根据Intel的官方文档第9.9.1节进入保护模式的步骤如下:

0)关闭中断

1)在程序中设置GDT(全局段描述符)

2)加载GDT到GDTR寄存器,LGDT

3)设置控制寄存器CR0.PE=1

4)ljmp到保护模式的地址执行——一定要执行它,用于清空之后的代码执行缓存。

关于段地址描述符:5.2节第3卷。

 然后boot.S 调用bootmain.为了调用它,因为它是c语言实现的所以需要先创建c语言运行环境——设置段寄存器与设置堆栈。

 当调用了bootmain之后,我们注意力就集中到了加载内核的步骤了。因为我们的内核是一个elf32格式的执行文件,而且目前系统没有任何加载器所以我们需要自己实现一个elf32的加载器,然后调转到内核进入点执行它。流程如下:

      1)加载内核到内存中
      为了加载内核到内存中,首先知道内核在哪儿,根据kern/Makefrag文件可以发现kernel镜像的创建方式如下:

$(OBJDIR)/kern/kernel.img: $(OBJDIR)/kern/kernel $(OBJDIR)/boot/boot

@echo + mk $@

$(V)dd if=/dev/zero of=$(OBJDIR)/kern/kernel.img~ count=10000 2>/dev/null

$(V)dd if=$(OBJDIR)/boot/boot of=$(OBJDIR)/kern/kernel.img~ conv=notrunc 2>/dev/null

$(V)dd if=$(OBJDIR)/kern/kernel of=$(OBJDIR)/kern/kernel.img~ seek=1 conv=notrunc 2>/dev/null
$(V)mv $(OBJDIR)/kern/kernel.img~ $(OBJDIR)/kern/kernel.img

     kernel在kernel.img的第2个扇区,第一个扇区为mbr。
     根据如上分析过程,令用bios的io指令读取硬盘的方法加载内核到地址0x10000。

     2)解析内核elf32头——详细的结构被inc/elf.h所定义

     当加载内核到内存中了,之后需要知道内核是什么。通过分析编译流程,发现内核其实生成在obj/kernel/kernel中,然后读取其头信息,知道内核为elf32文件(readelf -h obj/kern/kernel)。



      根据elf32的数据结构定义,解析加载的kernel得到需要加载的段到对应的物理地址,可以通过readelf -l obj/kern/kernel读取如下:



      如上的分析结果是通过读取elf文件所得的,而只需要将其以c语言代码的方式实现就可以了。
      3)跳转到内核进入点执行

     当加载完全之后,需要跳转到内核的入口地址来进入内核执行。通过如上的分析可以看出内核代码进入点为 物理地址0x10000c。

     对于内核的分析,还可以通过编译时所用的链接脚本来分析与理解:

<em><span style="font-size:12px;">/* Simple linker script for the JOS kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")——输出为elf32-i386的格式
OUTPUT_ARCH(i386)——构架为i386
ENTRY(_start)——进入点为_start,所以可通过nm obj/kernel/kernel|grep _start查看内核进入点

SECTIONS
{
/* Link the kernel at this address: "." means the current address */
. = 0xF0100000;——内核地址段开始的虚拟地址

/* AT(...) gives the load address of this section, which tells
the boot loader where to load the kernel in physical memory */
.text : AT(0x100000) {——将.text映射到0x100000的地方
*(.text .stub .text.* .gnu.linkonce.t.*)——.text段的内容
}

PROVIDE(etext = .);	/* Define the 'etext' symbol to this value */——设置链接变量,被程序读取得到代码段的长度

.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
}

/* Include debugging information in kernel memory */
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
BYTE(0)		/* Force the linker to allocate space
for this section */
}

.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
BYTE(0)		/* Force the linker to allocate space
for this section */
}

/* Adjust the address for the data segment to the next page */
. = ALIGN(0x1000);

/* The data segment */
.data : {——数据段的定义
*(.data)
}

PROVIDE(edata = .);

.bss : {——数据段未初始化的数据段
*(.bss)
}

PROVIDE(end = .);

/DISCARD/ : {
*(.eh_frame .note.GNU-stack)
}
}</span></em>


      关于elf的详细介绍可以参考如下:
https://pdos.csail.mit.edu/6.828/2012/readings/elf.pdf

    
3.内核初识

因为我们此次实验关心的是从开机到内核运行的流程,所以这次实现的内核只是一个简单的命令终端,而不具有任何实际内核的功能。而它可以作为uboot与grub的功能实现,因为它们就实现这样的终端功能。

当内核被启动时,依然是以汇编语言(kern/entry.S)开始,因为目前还不具备运行c语言的环境,所以需要汇编语言去创建之。

<em><span style="font-size:12px;">.globl		_start
_start = RELOC(entry)

.globl entry
entry:
movw	$0x1234,0x472			# warm boot

# We haven't set up virtual memory yet, so we're running from
# the physical address the boot loader loaded the kernel at: 1MB
# (plus a few bytes).  However, the C code is linked to run at
# KERNBASE+1MB.  Hence, we set up a trivial page directory that
# translates virtual addresses [KERNBASE, KERNBASE+4MB) to
# physical addresses [0, 4MB).  This 4MB region will be
# sufficient until we set up our real page table in mem_init
# in lab 2.

# Load the physical address of entry_pgdir into cr3.  entry_pgdir
# is defined in entrypgdir.c.
movl	$(RELOC(entry_pgdir)), %eax
movl	%eax, %cr3
# Turn on paging.
movl	%cr0, %eax
orl	$(CR0_PE|CR0_PG|CR0_WP), %eax
movl	%eax, %cr0

# Now paging is enabled, but we're still running at a low EIP
# (why is this okay?).  Jump up above KERNBASE before entering
# C code.
mov	$relocated, %eax
jmp	*%eax
relocated:

# Clear the frame pointer register (EBP)
# so that once we get into debugging C code,
# stack backtraces will be terminated properly.
movl	$0x0,%ebp			# nuke frame pointer

# Set the stack pointer
movl	$(bootstacktop),%esp

# now to C code
call	i386_init

# Should never get here, but in case we do, just spin.
spin:	jmp	spin

.data
###################################################################
# boot stack
###################################################################
.p2align	PGSHIFT		# force page alignment
.globl		bootstack
bootstack:
.space		KSTKSIZE
.globl		bootstacktop
bootstacktop:</span></em>


a)虚拟地址的使用

 因为所有程序实际使用的都是虚拟地址,而且根据kern/kernel.ld的地址对应关系发现程序的使用地址在0xF0000000之上。为此我们需要使用处理器提供的mmu机制——x86具体的分页与分段机制来实现虚拟地址的映射。

        根据intel的官方开发手册第3卷——9.8节,进入分页模式的步骤如下:

        0)进入32位保护模式

        1)设置至少4M空间的页表

        2)将页目录地址加载到CR3

        3)设置控制寄存器CR.PG=1

        4)ljmp到页地址进行执行,这一步可以清除执行的指令。

详细的分页介绍可以参考如下的相关概念:

       1)3种分页模式:32bit,PAE,IA-32,见表4-1

       2)页模式切换:第3卷-4.1.3节

         页的大小:CR4.PSE=1,4M,CR4.PSE=0,4k

       3)32bit模式的介绍:第三卷4.3节

          将线性地址分为3部分(G10,M10,L12):

         G10为页目录(PD)的索引,得到CR3|G10得到页表(PT)的基地址BPTE,

         M10为页表的索引,所以BPTE|M10得到物理页的基地址BPA,

         L12为物理地址偏移量,从而BPA|L12得到转换之后的物理地址。

         PDE的结构如表4.5所示

         PTE的结构如表4.6所示

         页目录的大小为1k,页表也为1k,页目录与页表必须4k对齐

        针对我们实际内核的实现,为了做到尽可能的简单,在我们映射空间只使用了低4M空间0x00000000-0x00400000。同时通过页目录的设置将0xf0000000之后的4M映射到0~4M的空间。可参考文件entrypgdir.c

<span style="font-size:12px;"><em>__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
// Map VA's [0, 4MB) to PA's [0, 4MB)
[0]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
[KERNBASE>>PDXSHIFT]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};</em> </span>


然后设置内核的堆栈,从而将c语言的新环境创建好了。

b)终端交互“接口”的实现——格式化输出(cprintf)的实现

最基本的交互方式是使用终端进行信息输出,所以实现c语言最基本的printf的功能是必要的。当然此时没有所谓的c库了,这也需要对编程环境有一个新的认识。而基本的信息输出,可以通过向串口与屏幕输出一个字符来实现。而格式化输出则只需要对输入的参数进行分析,然后依次输出到串口或者屏幕即可。基于此的分析,可以通过模块化分层设计的实现该功能,详细见下图。



      c)对堆栈的认识

       c语言的基本模块——函数都是基于堆栈实现,所以很有必要对堆栈有一个整体而全面的了解,特别是堆栈的状态,而且不仅仅是程序指令会对堆栈进行操作,而且硬件的某些事件也会对堆栈,从而将硬件的状态传递给程序。

首先需要了解intel处理器对堆栈的介绍,因为所有的程序运行都是基于intel的处理器,详情通过intel开发手册第一卷6.2节的介绍了解。需要详细介绍的有两个方面,如下:

      (1)6.2.1创建堆栈,所有堆栈都是这么建立:

     
i)设置堆栈段

     
ii)设置堆栈段描述符SS——可以通过MOV,POP,LSS指令实现

      iii)设置栈顶寄存器ESP——可以通过MOV,POP,LSS指令实现

      (2)堆栈帧的格式——由如图6.1表示:栈帧指向寄存器为EBP。

      


通过代码反汇编可以清晰的看到每一堆栈帧的具体格式与函数调用,参数传递的细节,如下以test_backtrace调用cprintf为例:

源代码如下:

void
test_backtrace(int x)
{
cprintf("entering test_backtrace %d\n", x);
if (x > 0)
test_backtrace(x-1);
else
mon_backtrace(0xf0, (char**)0xf1, (struct Trapframe*)0xf2);
cprintf("leaving test_backtrace %d\n", x);
}


反汇编部分如下:

void
test_backtrace(int x)
{
f0100040:	55                   	push   %ebp
f0100041:	89 e5                	mov    %esp,%ebp
f0100043:	53                   	push   %ebx
f0100044:	83 ec 0c             	sub    $0xc,%esp
f0100047:	8b 5d 08             	mov    0x8(%ebp),%ebx
cprintf("entering test_backtrace %d\n", x);
f010004a:	53                   	push   %ebx
f010004b:	68 20 1b 10 f0       	push   $0xf0101b20
f0100050:	e8 72 09 00 00       	call   f01009c7 <cprintf>
if (x > 0)
f0100055:	83 c4 10             	add    $0x10,%esp
f0100058:	85 db                	test   %ebx,%ebx
...........
}
int
cprintf(const char *fmt, ...)
{
f01009c7:	55                   	push   %ebp
f01009c8:	89 e5                	mov    %esp,%ebp
f01009ca:	83 ec 10             	sub    $0x10,%esp
va_list ap;
int cnt;
.......
}


通过如上的分析可以画出相应的堆栈局部来形象的描述该过程,这样也对我们分析程序运行与优化程序调用有指导性意义,如下图所示。



      基于处理器的堆栈理解,我们需要在内核代码中添加相关代码将如上的堆栈结构给打印出来,mon_backtrace需要实现。

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
unsigned int ebp = read_ebp();
struct Eipdebuginfo info;
char fn_name[64];
while(1){
cprintf("ebp 0x%x eip 0x%x args ",ebp,*(uint32_t*)(ebp+4));
for(int i=1;i<=3;i++){
cprintf(" 0x%x",*(uint32_t*)(ebp+4+4*i));
}
cprintf("\n");
if(ebp <KERNBASE)
break;
debuginfo_eip(*(uint32_t*)(ebp+4),&info);
cprintf(">>>>:eip[0x%x] at:",*(uint32_t*)(ebp+4));
memset(fn_name,0,64);
strncpy(fn_name,info.eip_fn_name,info.eip_fn_namelen);
if(*strfind(fn_name,'<')==0)
cprintf(" file:%s %s+0x%x\n",info.eip_file,fn_name,info.eip_line);
else
cprintf(" file:%s\n",info.eip_file);
ebp =*(uint32_t*)ebp;
}
return 0;
}


      输出的打印信息如下:

6828 decimal is XXX octal!

entering test_backtrace 5

entering test_backtrace 4

entering test_backtrace 3

entering test_backtrace 2

entering test_backtrace 1

entering test_backtrace 0

ebp 0xf010ff18 eip 0xf0100084 args  0xf0 0xf1 0xf2

>>>>:eip[0xf0100084] at: file:kern/init.c test_backtrace+0x44

ebp 0xf010ff38 eip 0xf0100068 args  0x0 0x1 0xf010ff78

>>>>:eip[0xf0100068] at: file:kern/init.c test_backtrace+0x28

ebp 0xf010ff58 eip 0xf0100068 args  0x1 0x2 0xf010ff98

>>>>:eip[0xf0100068] at: file:kern/init.c test_backtrace+0x28

ebp 0xf010ff78 eip 0xf0100068 args  0x2 0x3 0xf010ffb8

>>>>:eip[0xf0100068] at: file:kern/init.c test_backtrace+0x28

ebp 0xf010ff98 eip 0xf0100068 args  0x3 0x4 0x0

>>>>:eip[0xf0100068] at: file:kern/init.c test_backtrace+0x28

ebp 0xf010ffb8 eip 0xf0100068 args  0x4 0x5 0x0

>>>>:eip[0xf0100068] at: file:kern/init.c test_backtrace+0x28

ebp 0xf010ffd8 eip 0xf01000dd args  0x5 0x1aac 0x644

>>>>:eip[0xf01000dd] at: file:kern/init.c i386_init+0x40

ebp 0xf010fff8 eip 0xf010003e args  0x111021 0x0 0x0

>>>>:eip[0xf010003e] at: file:kern/entry.S

ebp 0x0 eip 0xf000ff53 args  0xf000e2c3 0xf000ff53 0xf000ff53

leaving test_backtrace 0

leaving test_backtrace 1

leaving test_backtrace 2

leaving test_backtrace 3

leaving test_backtrace 4

leaving test_backtrace 5

      当在分析stab信息时,为了方便理解可以用objdump -G obj/kernel/kernel将stab数组给dump出来。

      一叶说:完成实验1的“翻译”了。其中有很多习题需要回答与思考的,目前我没有列举到其中,不过,它们是值得我们去思考的。对于课程的很多细节的地方,需要使用readelf与objdump去辅助我们分析与理解,请参考之前的博客熟悉这些工具,当然,它们也是理解程序运行与程序结构的好助手值得花时间去熟悉之。同样,也有很多基础的功能实现,比如:读取键盘的输入,显示到终端与串口,我没有仔细去剖析,因为这些已经在前篇有详细的介绍,更重要的是本次实验主要精力在于系统的基本流程,这也是理解与创建操作系统的第一步。路漫漫其修远兮,吾将上下而求索。


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息