实验1正篇——引导PC

来源:互联网 发布:windows装mac双系统 编辑:程序博客网 时间:2024/06/05 08:13
      讲了这么久,终于进入主题了。为了让读者更深入的理解整个系统,做一些必要的铺垫是必需的;这样不仅能做到有理有据,更能让人知其然,而且知其所以然。题目所谓“正”篇,就是要从正面去描述该实验的内容,当然是对其进行意译,同时根据其内容,结合源码,提出自己的理解,相当于为实验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 startstart:  .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,$0x64seta20.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 modeprotcseg:  # 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 alignmentgdt:  SEG_NULL# null seg  SEG(STA_X|STA_R, 0x0, 0xffffffff)# code seg  SEG(STA_W, 0x0, 0xffffffff)        # data seggdtdesc:  .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">voidbootmain(void){struct Proghdr *ph, *eph;// read 1st page off diskreadseg((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)加载GDTGDTR寄存器,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

     kernelkernel.img的第2个扇区,第一个扇区为mbr

     根据如上分析过程,令用biosio指令读取硬盘的方法加载内核到地址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)——构架为i386ENTRY(_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.内核初识

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

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

<em><span style="font-size:12px;">.globl_start_start = RELOC(entry).globl entryentry: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)), %eaxmovl%eax, %cr3# Turn on paging.movl%cr0, %eaxorl$(CR0_PE|CR0_PG|CR0_WP), %eaxmovl%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, %eaxjmp*%eaxrelocated:# 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 pointermovl$(bootstacktop),%esp# now to C codecalli386_init# Should never get here, but in case we do, just spin.spin:jmpspin.data#################################################################### boot stack###################################################################.p2alignPGSHIFT# force page alignment.globlbootstackbootstack:.spaceKSTKSIZE.globlbootstacktop   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种分页模式:32bitPAEIA-32,见表4-1

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

         页的大小:CR4.PSE=14MCR4.PSE=04k

       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为例:

源代码如下:

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

反汇编部分如下:

void   test_backtrace(int x){f0100040:55                   push   %ebpf0100041:89 e5                mov    %esp,%ebpf0100043:53                   push   %ebxf0100044:83 ec 0c             sub    $0xc,%espf0100047:8b 5d 08             mov    0x8(%ebp),%ebxcprintf("entering test_backtrace %d\n", x);f010004a:53                   push   %ebxf010004b:68 20 1b 10 f0       push   $0xf0101b20f0100050:e8 72 09 00 00       call   f01009c7 <cprintf>if (x > 0)f0100055:83 c4 10             add    $0x10,%espf0100058:85 db                test   %ebx,%ebx...........}intcprintf(const char *fmt, ...){f01009c7:55                   push   %ebpf01009c8:89 e5                mov    %esp,%ebpf01009ca:83 ec 10             sub    $0x10,%espva_list ap;int cnt;.......}

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



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

intmon_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);elsecprintf(" 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去辅助我们分析与理解,请参考之前的博客熟悉这些工具,当然,它们也是理解程序运行与程序结构的好助手值得花时间去熟悉之。同样,也有很多基础的功能实现,比如:读取键盘的输入,显示到终端与串口,我没有仔细去剖析,因为这些已经在前篇有详细的介绍,更重要的是本次实验主要精力在于系统的基本流程,这也是理解与创建操作系统的第一步。路漫漫其修远兮,吾将上下而求索。


0 0
原创粉丝点击