从linux0.11引导代码小窥内存分段机制

 

       其实只是一点笔记,所以肯定会有错误,好在计算机科学是实践性很强的科学,一切都将以实验数据得出,我绝不会妄下结论,这也就减少了错误的发生。

       阅读本文手头上应该有一份linux0.11源代码,引导程序调试软件bochs(其实是个虚拟机,不过它的调试功能实在是完美)和配套的linux0.11内核imglinux-0.11-devel-040329.zip)。最好再有一本代码注释,推荐赵炯博士的《Linux内核完全注释——内核版本0.11》。显然,bochs的使用方法必须知道,具体操作请参阅《Linux内核完全注释》第14章;在bochs能够正确运行之后,使用bochsdbg进行调试,其使用方法见“Linux内核调试基本方法”。要想很好的理解操作系统,应具备一定的底层知识,推荐《深入理解计算机系统》。如果对老版本的linux很有兴趣,建议去OldLinux论坛,感谢赵炯博士的无私奉献。

调试环境的的建立

       下载linux-0.11-devel-040329.zip,解压缩到bochs的安装目录,其中包含一个bochs2.1.1的安装程序和linux内核img,找到bochsrc-hd.bxrc文件的1236行,修改其中的$ BXSHAREbochs的安装路径,如果就是上级目录,则可直接改为“..”,如:

12>           romimage: file=../BIOS-bochs-latest, address=0xf0000

编辑run.bat文件,将其中所有内容改为:

"../bochsdbg" -q -f bochsrc-Hd.bxrc

运行run.bat,即启动调试工具bochsdbg

实模式下的分段寻址

       0x0000:0x7c00处设置一个断点,在Linux引导程序开始处暂停。命令行如下:

<bochs:1> vbreak 0x0:0x7c00

<bochs:2> c

(0) Breakpoint 1, 0x7c00 (0x0:0x7c00)

Next at t=16252460

(0) [0x00007c00] 0000:7c00 (unk. ctxt): mov ax, 0x7c0             ; b8c007

       0x0000:0x7c00即以实模式下分段机制书写的逻辑地址,其物理地址的计算方法为0x0000<<4 + 0x7c00 = 0x00007c00,从输出信息可以看到程序在物理地址0x00007c00处暂停。

       引导程序一开始将其自身代码从0x7c00处复制到0x90000处,这个过程首先将0x7c0赋值给ds做为源数据段,将0x9000赋值给es做为目的数据段,即将ds:si的数据复制到es:di所在位置。现在通过调试来验证这一复制过程,以及进一步了解实模式下的分段行为。由ds:si => 0x7c0:0x0 => 0x7c0<<4+0x0 = 0x7c00,由es:di => 0x9000:0x0 => 0x9000<<4+0x0 = 0x90000,对比这两个绝对地址0x7c000x90000的数据即可验证上述复制过程。命令行如下:

<bochs:3> x /8 0x7c00

[bochs]:

0x00007c00 <bogus+       0>:    0x8e07c0b8      0x9000b8d8      0x00b9c08e

0x29f62901

0x00007c10 <bogus+      16>:    0xeaa5f3ff      0x90000018      0xd88ec88c

0xd08ec08e

<bochs:4> u /10

00007c00: (                    ): mov ax, 0x7c0             ; b8c007

00007c03: (                    ): mov ds, ax                ; 8ed8

00007c05: (                    ): mov ax, 0x9000            ; b80090

00007c08: (                    ): mov es, ax                ; 8ec0

00007c0a: (                    ): mov cx, 0x100             ; b90001

00007c0d: (                    ): sub si, si                ; 29f6

00007c0f: (                    ): sub di, di                ; 29ff

00007c11: (                    ): rep movsw word ptr es:[di], word ptr ds:[si] ;

 f3a5

00007c13: (                    ): jmp far 9000:0018         ; ea18000090

00007c18: (                    ): mov ax, cs                ; 8cc8

<bochs:5> break 0x7c13

<bochs:6> c

(0) Breakpoint 2, 0x7c13 in ?? ()

Next at t=16252723

(0) [0x00007c13] 0000:7c13 (unk. ctxt): jmp far 9000:0018         ; ea18000090

<bochs:7> x /8 0x90000

[bochs]:

0x00090000 <bogus+       0>:    0x8e07c0b8      0x9000b8d8      0x00b9c08e

0x29f62901

0x00090010 <bogus+      16>:    0xeaa5f3ff      0x90000018      0xd88ec88c

0xd08ec08e

       在反汇编代码中看到jmp far 9000:0018这一行,通过调试可看到其实际效果:将cs段设置为0x9000,从偏移量0x18处开始执行,也就是设置eip0x18。命令行如下:

<bochs:8> info r

……

eip            0x7c13           0x7c13

eflags         0x246            582

cs             0x0              0

……

<bochs:9> n

Next at t=16252724

(0) [0x00090018] 9000:0018 (unk. ctxt): mov ax, cs                ; 8cc8

<bochs:10> info r

……

eip            0x18             0x18

eflags         0x246            582

cs             0x9000           36864

……

       红色标记的cseip组合起来的值cs:eip即指向实模式下的代码逻辑位置。同样通过cs<<4 + eip来计算出实际地址。

建立GDT

       在保护模式下,段寄存器所存储的将是段描述符表的某个索引值,索引值指定的段描述符项中含有需要寻址的内存段的基地址、段的最大长度值和段的访问级别等信息。计算线性地址的示意图如下:

               1: 实模式和保护模式下寻址方式比较(摘自《Linux内核完全注释》)

       因此,在进入保护模式之前,需要建立GDT表,并让gdtr指向该表基址。在进入保护模式之前,Setup.s中的代码将设置GDT表,其指令为:

       end_move:

       mov ax,#SETUPSEG     ! right, forgot this at first. didn't work :-)

       mov ds,ax

       lidt   idt_48            ! load idt with 0,0

       lgdt  gdt_48           ! load gdt with whatever appropriate

       首先进入Setup程序(0x9020:0x0000处),找到lgdt gdt_48的指令位置,继续调试,命令行如下:

<bochs:11> break 0x90200

<bochs:12> c

(0) Breakpoint 3, 0x90200 in ?? ()

Next at t=16483610

(0) [0x00090200] 9020:0000 (unk. ctxt): mov ax, 0x9000            ; b80090

<bochs:13> u /100

……

0009029d: (                    ): lidt ds:0x12c             ; 0f011e2c01

000902a2: (                    ): lgdt ds:0x132             ; 0f01163201

……

<bochs:14> break 0x902a2

<bochs:15> c

(0) Breakpoint 4, 0x902a2 in ?? ()

Next at t=16750806

(0) [0x000902a2] 9020:00a2 (unk. ctxt): lgdt ds:0x132             ; 0f01163201

       反汇编代码lgdt ds:0x132表明将ds:0x132所在位置的数据赋给gdtrlgdt总共需要6个字节,其中两个字节为GDT表的长度,另外4个字节表明GDT表的基址。通过调试可以看到这条指令的实际作用,命令行如下:

<bochs:16> info r

……

ds             0x9020           36896

……

<bochs:17> xp /4 0x9020:0x132

[bochs]:

0x00090332 <bogus+       0>:    0x03140800      0x00000009      0x00000000

0x00000000

<bochs:18> n

Next at t=16750807

(0) [0x000902a7] 9020:00a7 (unk. ctxt): call .+0x109              ; e85f00

<bochs:19> dump_cpu

……

gdtr:base=0x90314, limit=0x800

idtr:base=0x0, limit=0x0

……

       0x00090332开始的8个字节分别是:0x03140800 0x00000009intel机器采用的小端法,即0x0009GDT表基址的高16位,0x0314GDT表基址的低16位,0x0800GDT表的长度。调试输出信息gdtr:base=0x90314, limit=0x800即验证这一结果。这些常数数据在Setup.s205224行定义。可以通过GDT表基址来查看一下GDT表,命令行如下:

<bochs:20> x /10 0x90314

[bochs]:

0x00090314 <bogus+       0>:    0x00000000      0x00000000      0x000007ff

0x00c09a00

0x00090324 <bogus+      16>:    0x000007ff      0x00c09200      0x00000000

0x08000000

0x00090334 <bogus+      32>:    0x00090314      0x00000000

       按照一个描述符8字节长度整理一下得:

0x00000000      0x00000000                  ! dummy

0x000007ff            0x00c09a00                  ! 内核代码段描述符

0x000007ff      0x00c09200                  ! 内核数据段描述符

0x00000000           0x08000000                  !

0x00090314           0x00000000                 ! GDT表项设置后紧接的idt_48,gdt_48的常数数据,在这个临时GDT表中无意义,实际也不会被索引到

       接下来将进入保护模式,并使用这个临时GDT表进行寻址。

保护模式下的分段寻址

       Setup.s中找到进入保护模式的代码:

       mov ax,#0x0001     ! protected mode (PE) bit

       lmsw      ax           ! This is it!

       jmpi 0,8          ! jmp offset 0 of segment 8 (cs)

       前两行指令设置保护模式比特位PE,第三行代码用保护模式下的寻址方式进行跳转。首先进入程序找到jmpi 0,8这一行代码所在位置,命令行如下:

<bochs:21> u /100

……

000902fe: (                    ): mov ax, 0x1               ; b80100

00090301: (                    ): lmsw ax                   ; 0f01f0

00090304: (                    ): jmp far 0008:0000         ; ea00000800

……

      

       jmp far 0008:0000指令的实际效果是设置cs0x0008,设置eip0x0000,这里的0x0008即为保护模式下的段选择符,写成二进制形式0000000000001000,前两位00表示特权级0,第三位0表示该选择符用于选择全局描述符表,高130000000000001表示使用全局描述符的第一项,即前面提到的内核代码段选择符:0x00007fff   0x00c09a000x00007fff表示这个段基址为0x0000,段限长0x7fff。调试验证这一分析结果,命令行如下:

<bochs:22> break 0x00090304

<bochs:23> c

(0) Breakpoint 5, 0x90304 in ?? ()

Next at t=16750869

(0) [0x00090304] 9020:00000104 (unk. ctxt): jmp far 0008:0000         ; ea000008

00

<bochs:24> n

Next at t=16750870

(0) [0x00000000] 0008:00000000 (unk. ctxt): mov eax, 0x10             ; b8100000

00

       在执行完jmp指令后,程序跳转到绝对地址0x00000000处,也就是保护模式下的逻辑地址0x0008:0x00000000,这实际上就是Head.s的代码了。Head.s的开始代码首先将dsesgsfs各个段寄存器的值设置为0x10,这个段选择符写成二进制形式:0000000000010000,它表示特权级0,选择全局描述符表的第2项,即前面提到的内核数据选择符:0x000007ff             0x00c09200,这个段基址为0x0000,段限长0x7fff

       Head.s一开始重新设置IDT表和GDT表,建立方法和在Setup.s中相差不大,下面来看看重新建立的GDT表。命令行如下:

<bochs:25> u /10

00000000: (                    ): mov eax, 0x10             ; b810000000

00000005: (                    ): mov ds, ax                ; 8ed8

00000007: (                    ): mov es, ax                ; 8ec0

00000009: (                    ): mov fs, ax                ; 8ee0

0000000b: (                    ): mov gs, ax                ; 8ee8

0000000d: (                    ): lss ds:0x192a4            ; 0fb225a4920100

00000014: (                    ): call .+0x6f               ; e856000000

00000019: (                    ): call .+0x9f               ; e881000000

0000001e: (                    ): mov eax, 0x10             ; b810000000

00000023: (                    ): mov ds, ax                ; 8ed8

<bochs:26> b 0x1e

<bochs:27> c

(0) Breakpoint 6, 0x1e in ?? ()

Next at t=16752168

(0) [0x0000001e] 0008:0000001e (unk. ctxt): mov eax, 0x10             ; b8100000

00

<bochs:28> dump_cpu

……

gdtr:base=0x5cb8, limit=0x7ff

idtr:base=0x54b8, limit=0x7ff

……

<bochs:29> x /10 0x5cb8

[bochs]:

0x00005cb8 <bogus+       0>:    0x00000000      0x00000000      0x00000fff

0x00c09a00

0x00005cc8 <bogus+      16>:    0x00000fff      0x00c09200      0x00000000

0x00000000

0x00005cd8 <bogus+      32>:    0x00000000      0x00000000

       这个GDT表的值是由Head.s代码末尾234行开始的常数数据定义的:

_gdt:       .quad 0x0000000000000000  /* NULL descriptor */

       .quad 0x00c09a0000000fff    /* 16Mb */

       .quad 0x00c0920000000fff   /* 16Mb */

       .quad 0x0000000000000000  /* TEMPORARY - don't use */

       .fill 252,8,0                   /* space for LDT's and TSS's etc */

       由此可以看到,这个GDT表的第0项未定义,第1项是内核代码段,第2项是内核数据段,第3项未定义,剩余的252项用于放置创建任务的局部描述符和任务状态段描述符。Linux内核使用的描述符表在内存中的示意图如下:

              2 Linux内核使用描述符表示意图(摘自《Linux内核完全注释》)

后记

       在建立完GDT表后,Head.s代码将继续进行分页,并开启分页机制,Linux将以分段机制将逻辑地址转换成线性地址,以分页机制将线性地址转换成物理地址。在其后的多任务作业时,GDT表末尾252项将得到填充使用。

       Linus有句名言:“Read the F**king code”,事实上以调试的方法来辅助阅读是相当有益的。

你可能感兴趣的:(c,linux,汇编,任务,Descriptor,linux内核)