MIT6.828-OS lab1:Booting a PC 记录

 

回归初心,从基础开始再搞起。

github:https://github.com/viktorika/mit-os-lab

环境配置

不多说,都是泪,千万别用64位系统搞这个东西,搞到最后我都搞不定。(PS:64位系统也可以搞,请参考这篇文章:https://www.dazhuanlan.com/2019/12/30/5e09a79eb2f81/

32位直接10分钟搞定了。基本如下。

1.先安装依赖库

libsdl1.2-dev

libtool-bin

libglib2.0-dev

libz-dev

libpixman-1-dev

2.把官方6.828的qemu clone下来然后执行下面命令安装

./configure --disable-kvm --disable-werror --target-list="i386-softmmu x86_64-softmmu"
make && sudo make install

3.按实验的pdf文档执行make和make qemu。找不到qemu的话需要修改conf/env.mk文件,把最后一个注释去掉,修改为qemu的路径,可以修改为QEMU=qemu-system-i386。然后就可以跑起来拉。

 

实验内容

早期处理器是16位的,所以只能分配1MB物理内存地址,Low Memory部分是个随机存取存储器(RAM)。1MB以外的其余部分用于特殊用途。后来处理器能够处理更大的内存了,但是为了向后兼容所以仍然保留low 1MB的内存空间,所以会出现一种情况就是现代电脑的物理内存会出现一个空洞在0X00A000到0x0010000之间,将RAM分割成low和conventional memory(即途中的Extended Memory)两个部分。此外,32位电脑顶部还会预留一部分空间,现在通常由BIOS保留供32位PCI设备使用。

如今的内存已经可以突破4GB以上了,因此这里会出现第二个空洞,理由和上面一样为了向后兼容。但是这个实验里只会使用前256MB的内存,假定是个只有32位的机子。

 

PART1

开始启动,开两个终端,一个执行make qemu-gdb,另一个执行gdb,然后会生成一个.gdbinit文件,这个文件会关联到正在运行的qemu,然后文件里可以看到有一行:[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b。这是gdb对执行文件的第一条指令的反汇编,可以看到这个PC开始执行的物理地址是0x000ffff0,然后他的第一个指令是一个长跳转指令,跳到CS(code segment)=0xf000,IP=0xe05b的地方。物理地址的计算公式:physical address = 16 * segment + offset。

BIOS运行时:

1.建立中断描述符表,并且初始化各种设备。

2.在初始化完PCI总线和BIOS知道的所有重要设备后,会搜索可引导设备,软盘、硬盘、CD-ROM等等,最终找到可引导的磁盘设备后,会读取引导加载程序然后把控制权给引导加载程序。

 

PART2

PC的软盘和硬盘被一个叫扇区的大小为512个字节的东西分成很多块。扇区是磁盘的最小传输粒度。每次读写操作都必须在一个或多个扇区里操作,并且在扇区边界上对齐。如果磁盘是可引导的,那么第一个扇区称为可引导扇区,因为这是引导加载程序代码所在的位置。当BIOS找到一个可启动软盘或硬盘,它将512字节的引导扇区加载到物理地址0x7c00至0x7dff的内存中,然后使用jmp指令将CS:IP设置为0000:7c00,将控制权传递给引导加载程序。

现代的BIOS从CD-ROM中引导的方式更为复杂,CD-ROM一个扇区是2048字节。在BIOS将控制权给磁盘之前可以将更大的引导镜像加载到内存里(不仅仅在一个扇区里)。

这个实验是用常规的硬盘启动机制。引导加载程序由一个汇编语言源文件boot/boot.S和一个C源文件boot/main.c组成。引导加载程序必须执行两个重要的功能:

1.首先,引导加载程序必须从real mode切换到32-bit protected mode,因为只有在这个模式下,软件才能访问1MB以上的所有物理内存。在这个模式下,分段地址segment:offset转换为物理地址的方式有所不同,并且转换后偏移量是32位的而不是16位的。

2.其次,引导加载程序通过x86的特殊I / O指令直接访问IDE磁盘设备寄存器,从而从硬盘读取内核。

obj/boot/boot.asm文件,这个文件是makefile编译引导加载程序的时候生成的反汇编。obj/kern/kernel.asm包含了一系列JOS kernal的反汇编,这些文件都是常常用来debug的。

接着可以通过boot/main.c trace到bootMain函数里,然后再trace到readsect函数,找到函数循环的入口和出口,设个断点,用continue命令跳出循环,然后逐步完成引导加载程序的其余部分。然后回答下面三个问题:

question 1:在哪里从16位模式切换到32位模式的?

answer: movl %eax, %cr0,cr0寄存器是控制寄存器,为1则是preotect mode,为0是real mode。(PS:查询资料所得)

question 2:引导加载程序的最后一条指令是?

answer:最后一条指令应该就是boot.asm里main的最后一句 call *0x10018,根据注释可以知道这个函数调用的地址是ELF文件头的入口指针。

question 3:kernel执行的第一条指令是?

answer:kernel的第一条指令就是这条指令的下一条指令,也就是movw $0x1234 $0x472,也可以直接看kern/entry.S文件得知。

question 4:引导加载程序如何知道需要读取几个扇区才能获取整个内核?他在哪里得到这个信息?

answer:内核本质上就是一个program,所以跟一般program一样是从ELF头获取需要知道的信息的,从main.c的代码上看的话应该是通过e_phoff和e_phnum来计算内核程序段头的起点和终点,然后根据程序段头的信息把各个程序段load进内存。

为了理解boot/main.c文件,需要了解ELF二进制文件是什么,编译C程序从.c文件到.o文件(目标文件),再链接所有.o成ELF文件。(PS:根据编译原理所说,理论上其实是四个步骤,预处理-编译-汇编-链接,每一个步骤gcc都是可以用命令执行的)

ELF格式的大多数复杂部分都是用于共享库的动态加载,在这个实验中不需要太过深入了解。

对于6.828,可以将ELF可执行文件视为具有加载信息的标头,然后是几个程序段,每个程序段都是要在指定地址加载到内存中的连续代码或数据块。 引导加载程序不会修改代码或数据; 它将其加载到内存中并开始执行它。

ELF文件有个固定长度的ELF头,然后是可变长度的程序头,该头列举了要加载的程序段,ELF的标头定义在inc/elf.h文件中。关心的几个段如下:

.text:程序的可执行指令

.rodata:只读数据,例如C编译器生成的ASCII字符串常量。 (但是,不会麻烦设置硬件来禁止写入。)

.data:初始化的全局变量,例如声明了全局变量int x = 5。

.bss:紧跟.data后面,在链接器计算内存布局的时候会预留空间给未初始化的全局变量,c要求未初始化的全局变量的值是0,所以无需在ELF二进制文件中存储.bss的内容,反而是链接器仅记录.bss节的地址和大小。 加载程序或程序本身必须将.bss节清零。

(PS:印象中记得还有heap和stack段)

可以通过i386-jos-elf-objdump -h obj/kern/kernel来查看程序段,在linux上的话直接用objdump代替i386-jos-elf-objdump。

要特别注意.text段的VMA和LMA。

通常,VMA和LMA是相同的。比如,引导加载程序的.text段。

BIOS将引导加载程序加载到0x7c00,所以引导加载程序的LMA就是0x7c00,这也是执行引导扇区的位置,所以也是VMA,这里地址链接是通过 -Ttext 0x7C00传递给boot / Makefrag中的链接器来设置链接地址。

 

question:再次遍历引导加载程序的前几条指令,并确定第一条可能“中断”或执行错误操作(弄错了引导加载程序的链接地址的情况下)的指令,尝试在boot/Makefrag里修改VMA,然后make clean清空,再make重新编译,然后再次trace引导加载程序,观察一下会发生什么。

answer:大概,都是会卡在这条指令上。

这条指令是切换到protect mode后的第一条指令,为什么会卡在这呢?根据我查阅一大堆资料,最终得出的结论如下:

在这之前有一条指令将GDT地址放进某寄存器。

    

GDT是全局描述符表,在real mode下内存寻址可以直接通过上面所说的segment<<4+offeset来计算,但是在protect mode的情况下不行,具体请自己查询资料,简单来说切换到protect mode后,段寄存器里存的只是个索引,要从GDT中根据这个索引找到段的基地址,再加上offset得到LMA,GDT的地址放到GDTR寄存器。这里修改VMA导致和LMA不一致,VMA上没有将LMA的内容拷贝过去,所以就找不到指令

 

回顾内核的VMA和LMA,和引导加载程序不同,他们的VMA和LMA是不一样的.

有一个ELF头字段对我们来说也是很重要的,叫e_entry,这个字段保存program的入口VMA:就是program在text段里开始执行的内存地址。

 

question:重置机器,在0x00100000的内存地址查看16个字节在BIOS进入到boot loader和boot loader进入到kernel,观察这两个时间点内存的位置有什么变化?(不需要真的实验,请自己思考一下)

answer:思考结果:BIOS切到boot loader的时候还没切到protect 模式没法用1MB以上的内存估这里应该没有变化,而从boot loader切到kernel时,kernel的入口指令会放在1MB这个地方,因此内存里的内容会变成对应的指令。

 

PART3

现在将开始研究最小的jos内核,(最终你需要写一些代码),内核代码从汇编代码开始,这些代码进行设置以便C语言代码可以正确运行。

你会发现boot loader的VMA和LMA是完美匹配的,但是kernel的VMA和LMA是完全不一样的。

操作系统内核喜欢链接到非常高的虚拟地址去执行,比如说0xf0100000,为了保留虚拟地址的下部分给用户进程使用。这样安排的原因会在下个lab详细说明。

许多机器在地址0xf0100000上没有任何物理内存,因此我们不能指望能够在其中存储内核。取而代之,我们使用处理器的内存管理硬件将虚拟地址0xf0100000映射到物理地址0x00100000。这种方法,虽然内核的虚拟地址很高留下大量的空间给用户进程,他还是会加载到PC的1MB的RAM的地方,刚好在BIOS的正上方,这种手段要求PC至少有几m的物理内存,保证物理内存在0x00100000的地方可以正常工作,在1990年后这是很容易可以实现的。

事实上,下一个实验将映射PC的整个底部256MB物理地址空间,从物理地址0x00000000到0x0fffffff,到虚拟地址0xf0000000到0xffffffff,所以现在你应该明白为什么JOS只能使用前256MB的物理内存。

现在,我们仅仅映射4MB的物理内存,这足够让我们启动并且运行。我们使用手写的,静态初始化的页面目录和kern / entrypgdir.c中的页面表来执行此操作。现在,你不需要知道太多的细节关于这一块是怎么工作的,只需要了解效果就行了,直到kern / entry.S设置CR0_PG标志为止,内存引用都被视为物理地址(严格来说是线性地址,但是boot / boot.S设置了从线性地址到物理地址的映射, 并且永远不会改变他),一旦CR0_PG被设置,内存引用就是虚拟地址,这些地址由虚拟内存硬件转换为物理地址。 entry_pgdir将范围从0xf0000000到0xf0400000的虚拟地址转换为物理地址0x00000000到0x00400000,并将虚拟地址0x00000000到0x00400000转换为物理地址0x00000000到0x00400000,不在这两个范围的虚拟地址都会使硬件异常,由于实验没有设置中断处理,就会导致qemu转储计算机状态并退出(或者没有使用补丁版本的话就会无限重启)。

 

question:使用qemu和gdb跟踪JOS内核停在movl %eax, %cr0,检查0x00100000和0xf0100000的内存,然后单步调试,再次检查0x00100000和0xf0100000的内存,确保你明白发生了什么。

answer:回答:找到对应代码查看注释可得,这句话是设置cr0的Paging Enable位,这涉及到cr0寄存器的另一个功能,一旦设置了该位,芯片上的分页部件就会开始工作,分页开始工作后才开始把虚拟地址映射到物理地址,所以这句语句执行前因为没有分页所以内容等于分段寻址后物理地址的内容,分页功能开启后。需要详细了解三个地址,逻辑地址,线性地址以及物理地址。

 

question:建立新映射后的第一条指令是什么,如果没有正确的映射将无法正常工作? 注释掉kern / entry.S中的%eax动画,%cr0,跟踪它,然后查看您是否正确。

answer:这个不用试都知道肯定报错。。。就跟上面的其中一个问题很像,你如果把分页功能关掉那么他就会认为线性地址等于物理地址,那么你就会访问到未知内容的物理地址,肯定报错。

 

大多数人都将诸如printf()之类的函数视为理所当然,有时甚至将它们视为C语言的“原始”。 但是在OS内核中,我们必须自己实现所有I / O。

尝试修改文件,使其能够输出%o格式。

这个很简单,找到对应的文件后修改如下:

                case 'o':
                        // Replace this with your code.
                        //putch('X', putdat);
                        //putch('X', putdat);
                        //putch('X', putdat);
                        //break;
                        num = getuint(&ap, lflag);
                        base = 8;
                        goto number;

做完后回答下面的问题:

question1:解释一下print.c和console.c之间的接口。具体来说,console.c提供什么函数? printf.c如何使用此函数?

answer:粗略看了一下,console.c有一大堆直接操作终端IO的函数,但是都是static的,故外部不能调用,非static部分则是提供给外部使用的一个高级接口,print.c则是使用这些接口来输出到控制台。

question2:解释下面来自console.c的代码。

answer:从上下文推导,crt_pos为当前屏幕的位置,CRT_SIZE为屏幕能够显示的数量,crt_pos>=CRT_SIZE证明当前输出量溢出屏幕了,memmove则是把缓冲区crt_buf的内容去掉第一行,然后最后一行的内容用0x0700|‘ ’来代替,然后更新crt_pos。

question3:在cprintf()的调用中,fmt指向什么? ap指向什么?

列出(按执行顺序)对cons_putc,va_arg和vcprintf的每个调用。 同时对于cons_putc,列出其参数。 对于va_arg,请列出ap在调用前和调用后指向的内容。 对于vcprintf,列出其两个参数的值。

answer:很显然,fmt指的是"x %d, y %x, z %d\n",ap是va_list类型,这是c语言用于指向不定参数的类型,va_start是将位置定位到当前参数的后一个参数。故最后ap指的是x的地址。调用顺序的话很显然是vcprintf会调用vprintfmt,vprintfmt里会用va_arg获得参数,并且会调用putch继而调用cons_putc来输出到屏幕。cons_putc就一个参数,表示输出的字符。va_arg调用前后指针会向后移动sizeof(type)个字节,vcprintf的两个参数的值刚刚上面也顺便解答了。

question4:跑一下下面的代码。

输出结果是什么,用单步方式解释一下这个输出结果。输出取决于x86是little-endian的事实。 如果x86是big-endian,那么您将i设置为什么才能产生相同的输出? 您是否需要将57616更改为其他值?

answer:输出结果为He110,world,如果是big-endian的话,i应该设置为0x726c6400,57616不用修改,因为%x对是bit-endian和little-endian没有依赖。

question5:在下面的代码中,y=后面会输出什么?为什么会发生这种事?

answer:无法确定,得看越界后的栈的位置上存的是什么。

question6:假设GCC更改了其调用约定,以使其按声明顺序将参数压入栈,以便最后一个参数最后压入。 您将如何更改cprintf或其接口,以便仍然可以向其传递可变数量的参数?

answer:这个直接把va_list提供的几个宏修改修改,就是把地址加法改成减法,因为栈顶在低地址。

后面还有个challenge。。。。我就不做了。有兴趣可以看pdf搞。

 

这次实验的最后一个练习,将探索x86的c语言使用栈的更多细节。在这个过程中,会写一个有用的内核监视方法用来打印系统栈:嵌套调用指令中导致当前执行点的已保存指令指针(IP)值的列表(这句真不知道怎么翻译,直接抄google好了),

question:确定内核将栈初始化的位置,栈在内存中的位置,内核如何为栈预留空间,栈指针在预留空间的底部初始化的时候指向哪里?

answer:栈顶是sp,基指针是bp,根据这两个来找的话很容易就找到了。就是下图这个位置初始化了栈:

上面可以得到栈的逻辑地址是0xf0110000。

看到上面可知,通过.space分配一块大小为KSTKSIZE的连续的空间给栈使用。栈的结尾可以看到有一段用于对齐的部分,就是指向那里了。要是对齐部分为0那么应该是指向.text段结尾吧。这里也可以知道栈是放在kernel的.data段,当然我不确定其他的kernel是否是这样的。。

x86的栈指针(esp寄存器)指向当前使用的栈的最低位置。这个位置下面的预留空间的区域都是还未使用的空间,把一个值压到栈里涉及到将栈指针减小以及将这个值放到当前栈指针指向的位置。出栈则相反。在32位模式里,栈只能容纳32位的值,esp总是可以被4整除,各种x86指令(例如调用)被“硬连线”以使用栈指针寄存器。

相比之下,ebp(基指针)寄存器主要通过软件约定与堆栈相关联,比如进入C函数时,该函数的开始的指令通常通过将前一个函数的基址指针压入栈来保存该基址指针,然后在函数持续时间内将当前esp值复制到ebp中(简单来说就是一个push指令和一个move指令),如果所有的程序都遵守这个惯例,那么在任何时间点通过保存的ebp指针的链去跟踪栈并且确定到达这个点的函数调用序列。这个能力特别有用,比如当一个特别的函数因为一些不好的参数引发assert失败或者panic,但是你不确定谁传了参数,这时候你可以通过回溯栈让你找到问题。

 

question:为了熟悉c在x86的调用规范,在obj/kern/kernel.asm找到test_backtrace的函数地址,在这里设个断点,检查kernel启动后,每次调用该函数会发生什么情况,每个递归调用层级在压入栈的时候有多少32位的字,这些字都是什么?

answer:其实。。。也没必要设断点调了,无非就是进入函数前要压参数进栈,(还有一些调用者保护寄存器进栈),返回地址进栈,进入函数后,被调用者首先做的是bp压栈,以便还原,bp指针修改为当前sp的值,(被调用者寄存器入栈),然后sp指针下移一定个单位的空间为栈帧里的局部变量预留空间。可以对着kernel.asm来看就好,比如说找到下面两个地方。

为了练习能够好好完成,你应该使用在工具页上的补丁版qemu,否则你不得不手动转换所有的断点和内存地址为线性地址,上面的练习应该为您提供实现栈回溯函数(调用mon_backtrace())所需的信息,此函数的原型已经在kern/monitor.c等着你,您可以完全在C中完成此操作,但是您可能会发现inc/x86.h中的read_ebp()函数很有用。 您还必须将此新函数挂接到内核监视器的命令列表中,以便用户可以交互地调用它。

回溯函数应该以以下的格式展示一系列的函数调用帧。

打印的第一行反映了当前正在执行的函数,即mon_backtrace本身,第二行反映了称为mon_backtrace的函数,第三行反映了调用该函数的函数,依此类推。 您应该打印所有未完成的栈帧。 通过研究kern/entry.S,您会发现有一种简单的方法可以告诉您何时停止。

在每行中,ebp值指示该函数使用的栈中的基指针:即,刚输入该函数并设置函数开始代码之后的栈指针的位置。列出的eip值是函数的返回指令指针:当函数返回时控制将要返回的指令的地址,通常指向调用它的指令的下一条指令的地址。最后,在args之后列出的五个十六进制值是这些函数的前五个参数,在调用该函数之前,这些参数已被压入栈。当然,如果使用少于五个参数调用该函数,则并非所有这五个值都将是有用的。

question:为什么回溯代码不能检测有多少个参数呢?如何解决此限制?

answer:因为回溯的时候是从调用函数的基地址往上加来获取参数的,你不能知道在调用之前他push了几个参数,所以你也不知道参数什么时候到底,也无法知道这些参数的字节大小,要解决这个问题我觉得只能加个长度参数放在第一个参数的位置。。。我查阅了很多资料好像都没有提这个问题,如果有不需要在每个函数的第一个参数加个长度的方法,请告诉我一下,我很好奇谢谢。

练习:完成上面所说得backtrace函数。用make grade来查看输出是否符合期望,如果没有则请修改到正确为止。

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{   
    cprintf("Stack backtrace:\n");
    unsigned int ebp, esp, eip;
    ebp = read_ebp();
    while(ebp){
        eip = *(unsigned int *)(ebp + 4);
        esp = ebp + 4;
        cprintf("ebp %08x eip %08x args", ebp, eip);
        for(int i = 0; i < 5; ++i){
            esp += 4;
            cprintf(" %08x", *(unsigned int *)esp);
        }   
        cprintf("\n");
        ebp = *(unsigned int *)ebp;
    }   
    return 0;
}

不知道 为啥我这边机子跑make grade超时,我copy过来的lab的脚本不会终止输入,卡在了select,经过我一番折腾,问题应该是在于kill命令不知道为什么没把qemu给kill掉,所以我这边只好弄多一个窗口make grade的时候输入完把qemu kill掉。。。有可能是gdb版本的问题识别不了这个命令?反正这命令加密了我也改不动,也不想搞了,能跑通就行。。

 

通常我们使用mon_backtrace还想知道其他信息,比如函数名之类的,所以我们还要完善它,这里提供了一个debuginfo_eip函数,他在kern/kdebug.c文件里。

练习:这是最后一个练习了,修改mon_backtrace函数,通过使用debuginfo_eip来打印eip的相关信息。我们还需要完成debuginfo_eip函数。要求输出的格式变成这样:

拿第一个例子说,kern/monitor.c是eip所在的源文件,143是行号,monitor是所在的函数,106是eip对应的指令的地址相对于函数的偏移量。

要解决这个问题,首先我们得知道两个概念,符号表和字符串表。

符号表存着所有符号的信息,比如函数,变量,指令等等。其中符号的名字并不是直接村在符号表的每个表项里,表项里存的是偏移量,这个偏移量是相对于字符串表的偏移量。

字符串表就是一个数组,数组里面通过多个\0来隔开多个字符串,所以你只要知道偏移量来确定你需要的字符串的开头在字符串表的哪个地方就可以得到这个字符串。

关于这两个的详细信息请查阅其他资料。(PS:毕竟本人也不懂,上面只是用我的话简单来说明一下我对这两个的了解以及它们的关系)。

然后我们来看一下debuginfo_eip函数。

先看注释,大概就是说你传一个指令的地址addr进来,成功的话会把结果存在一个Eipdebuginfo结构的info信息给你,并且返回0.

下面分析一下代码,先看开头。

这里其实没什么好说,初始化+安全性检查。addr >= ULIM这个判定应该是内核用的是高地址部分。再看下面。

这里是在符号表里查找包含该addr的源文件表项,具体stab_binsearch我就不多说了吧,看注释就能看明白的。再看下面吧。

这回是在符号表里找包含addr地址的函数表项,并且把源文件信息和相关的函数名信息存到info里。

因为我做完才写的博客,直接把代码截进来了,这里是查找addr对应的指令的符号表项,找到再把行号的信息存在info里,这个还是蛮容易写的。再后面的代码就不解释了(其实是我自己没怎么看懂,大佬们看注释自己琢磨吧。。)。

然后再回过头来写这个mon_backtrace,基本上很好写了。代码如下:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
    cprintf("Stack backtrace:\n");
    unsigned int ebp, esp, eip;
    ebp = read_ebp();
    while(ebp){
        eip = *(unsigned int *)(ebp + 4);
        esp = ebp + 4;
        cprintf("ebp %08x eip %08x args", ebp, eip);
        for(int i = 0; i < 5; ++i){
            esp += 4;
            cprintf(" %08x", *(unsigned int *)esp);
        }
        cprintf("\n");
        struct Eipdebuginfo info;
        if (-1 == debuginfo_eip(eip, &info))
            break;
        cprintf("%s:%d: %.*s+%u\n", info.eip_file, info.eip_line, info.eip_fn_namelen, info.eip_fn_name, eip - info.eip_fn_addr);
        ebp = *(unsigned int *)ebp;
    }
    return 0;
}

最后make grade通过。

 

总结

1.物理内存分为low和conventional memory,BIOS用的内存是low的一部分,由于物理内存中各个部分都有一些地方是给特殊设备用的,所以每当内存扩展的时候都会出现空洞(即用户程序无法使用的物理内存部分)。

2.计算机的启动过程:

大体方向上来说是BIOS->boot loader->kernel。

BIOS启动时主要会做以下事情:

       1)建立中断描述符表,并且初始化各种设备。

       2)在初始化完PCI总线和BIOS知道的所有重要设备后,会搜索可引导设备,软盘、硬盘、CD-ROM等等,最终找到可引导的磁盘设备后,会读取引导加载程序然后把控制权给引导加载程序。

boot loader主要会做以下事情:

       1)从real mode切换到protect mode。

       2)其次,引导加载程序通过x86的特殊I / O指令直接访问IDE磁盘设备寄存器,从而从硬盘读取内核。

3.在real mode模式里,cs:ip寄存器找物理地址的方法是:physical address = 16 * segment + offset。在protect mode里cs寄存器是存的是索引,然后去查段表加上偏移量获得物理地址。(这里针对的是指令,不同段用不同的寄存器)

4.磁盘的最小传输粒度是扇区,硬盘被分为n个扇区,每个扇区大小位512字节(现在的磁盘应该不是512了,但是这个实验是512)。如果磁盘可引导,那么第一个扇区被称为可引导扇区。

5.逻辑地址:程序所使用的地址通过查段表(如果是real mode则直接计算)转换为->线性地址:然后再查页表转换为->物理地址。

6.符号表存着所有符号的信息,比如函数,变量,指令等等。其中符号的名字并不是直接存在符号表的每个表项里,表项里存的是偏移量,这个偏移量是相对于字符串表的偏移量。字符串表就是一个数组,数组里面通过多个\0来隔开多个字符串,所以你只要知道偏移量来确定你需要的字符串的开头在字符串表的哪个地方就可以得到这个字符串。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值