由linux0.11代码小窥内存分段机制(转)

http://blog.csdn.net/lijingze2003/archive/2005/03/25/330529.aspx
       阅读本文手头上应该有一份
linux0.11源代码
,引导程序调试软件
bochs
(其实是个虚拟机,不过它的调试功能实在是完美)和配套的
linux0.11内核img
(linux-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文件的12、36行,修改其中的$ BXSHARE为bochs的安装路径,如果就是上级目录,则可直接改为“..”,如:
12>           romimage: file=..\BIOS-bochs-latest, address=0xf0000
编辑run.bat文件,将其中所有内容改为:
"..\bochsdbg" -q -f bochsrc-Hd.bxrc
运行run.bat,即启动调试工具bochsdbg。
实模式下的分段寻址
       在0x0000:0x7c00处设置一个断点,在Linux引导程序开始处暂停。命令行如下:
vbreak 0x0:0x7c00

c

(0) Breakpoint 1, 0x7c00 (0x0:0x7c00)
Next at t=16252460
(0) [0x00007c00] 0000:7c00 (unk. ctxt): mov ax, 0x7c0             ; b8c007
       0x0000:0x7c00即以实模式下分段机制书写的逻辑地址,其物理地址的计算方法为0x00007c00 = 0x00007c00,从输出信息可以看到程序在物理地址0x00007c00处暂停。
       引导程序一开始将其自身代码从0x7c00处复制到0x90000处,这个过程首先将0x7c0赋值给ds做为源数据段,将0x9000赋值给es做为目的数据段,即将ds:si的数据复制到es:di所在位置。现在通过调试来验证这一复制过程,以及进一步了解实模式下的分段行为。由ds:si => 0x7c0:0x0 => 0x7c07c00,由es:di => 0x9000:0x0 => 0x9000,对比这两个绝对地址0x7c00和0x90000的数据即可验证上述复制过程。命令行如下:
x /8 0x7c00

[bochs]:
0x00007c00        0>:    0x8e07c0b8      0x9000b8d8      0x00b9c08e
0x29f62901
0x00007c10       16>:    0xeaa5f3ff      0x90000018      0xd88ec88c
0xd08ec08e
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
break 0x7c13

c

(0) Breakpoint 2, 0x7c13 in ?? ()
Next at t=16252723
(0) [0x00007c13] 0000:7c13 (unk. ctxt): jmp far 9000:0018         ; ea18000090
x /8 0x90000

[bochs]:
0x00090000        0>:    0x8e07c0b8      0x9000b8d8      0x00b9c08e
0x29f62901
0x00090010       16>:    0xeaa5f3ff      0x90000018      0xd88ec88c
0xd08ec08e
       在反汇编代码中看到jmp far 9000:0018这一行,通过调试可看到其实际效果:将cs段设置为0x9000,从偏移量0x18处开始执行,也就是设置eip为0x18。命令行如下:
info r

……
eip            0x7c13           0x7c13
eflags         0x246            582
cs             0x0              0
……
n

Next at t=16252724
(0) [0x00090018] 9000:0018 (unk. ctxt): mov ax, cs                ; 8cc8
info r

……
eip            0x18             0x18
eflags         0x246            582
cs             0x9000           36864
……
       红色标记的cs和eip组合起来的值cs:eip即指向实模式下的代码逻辑位置。同样通过cs来计算出实际地址。
建立GDT表
       在保护模式下,段寄存器所存储的将是段描述符表的某个索引值,索引值指定的段描述符项中含有需要寻址的内存段的基地址、段的最大长度值和段的访问级别等信息。计算线性地址的示意图如下:

[url=http://blog.csdn.net/images/blog_csdn_net/lijingze2003/28214/r_实模式与保护模式下寻址方式的比较.JPG][/url]
               图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的指令位置,继续调试,命令行如下:
break 0x90200

c

(0) Breakpoint 3, 0x90200 in ?? ()
Next at t=16483610
(0) [0x00090200] 9020:0000 (unk. ctxt): mov ax, 0x9000            ; b80090
u /100

……
0009029d: (                    ): lidt ds:0x12c             ; 0f011e2c01
000902a2: (                    ): lgdt ds:0x132             ; 0f01163201
……
break 0x902a2

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所在位置的数据赋给gdtr,lgdt总共需要6个字节,其中两个字节为GDT表的长度,另外4个字节表明GDT表的基址。通过调试可以看到这条指令的实际作用,命令行如下:
info r

……
ds             0x9020           36896
……
xp /4 0x9020:0x132

[bochs]:
0x00090332        0>:    0x03140800      0x00000009      0x00000000
0x00000000
n

Next at t=16750807
(0) [0x000902a7] 9020:00a7 (unk. ctxt): call .+0x109              ; e85f00
dump_cpu

……
gdtr:base=0x90314, limit=0x800
idtr:base=0x0, limit=0x0
……
       0x00090332开始的8个字节分别是:0x03140800 0x00000009,intel机器采用的小端法,即0x0009为GDT表基址的高16位,0x0314为GDT表基址的低16位,0x0800为GDT表的长度。调试输出信息gdtr:base=0x90314, limit=0x800即验证这一结果。这些常数数据在Setup.s的205到224行定义。可以通过GDT表基址来查看一下GDT表,命令行如下:
x /10 0x90314

[bochs]:
0x00090314        0>:    0x00000000      0x00000000      0x000007ff
0x00c09a00
0x00090324       16>:    0x000007ff      0x00c09200      0x00000000
0x08000000
0x00090334       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这一行代码所在位置,命令行如下:
u /100

……
000902fe: (                    ): mov ax, 0x1               ; b80100
00090301: (                    ): lmsw ax                   ; 0f01f0
00090304: (                    ): jmp far 0008:0000         ; ea00000800
……
      
       jmp far 0008:0000指令的实际效果是设置cs为0x0008,设置eip为0x0000,这里的0x0008即为保护模式下的段选择符,写成二进制形式0000000000001000,前两位00表示特权级0,第三位0表示该选择符用于选择全局描述符表,高13位0000000000001表示使用全局描述符的第一项,即前面提到的内核代码段选择符:0x00007fff   0x00c09a00,0x00007fff表示这个段基址为0x0000,段限长0x7fff。调试验证这一分析结果,命令行如下:
break 0x00090304

c

(0) Breakpoint 5, 0x90304 in ?? ()
Next at t=16750869
(0) [0x00090304] 9020:00000104 (unk. ctxt): jmp far 0008:0000         ; ea000008
00
n

Next at t=16750870
(0) [0x00000000] 0008:00000000 (unk. ctxt): mov eax, 0x10             ; b8100000
00
       在执行完jmp指令后,程序跳转到绝对地址0x00000000处,也就是保护模式下的逻辑地址0x0008:0x00000000,这实际上就是Head.s的代码了。Head.s的开始代码首先将ds,es,gs,fs各个段寄存器的值设置为0x10,这个段选择符写成二进制形式:0000000000010000,它表示特权级0,选择全局描述符表的第2项,即前面提到的内核数据选择符:0x000007ff             0x00c09200,这个段基址为0x0000,段限长0x7fff。
       Head.s一开始重新设置IDT表和GDT表,建立方法和在Setup.s中相差不大,下面来看看重新建立的GDT表。命令行如下:
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
b 0x1e

c

(0) Breakpoint 6, 0x1e in ?? ()
Next at t=16752168
(0) [0x0000001e] 0008:0000001e (unk. ctxt): mov eax, 0x10             ; b8100000
00
dump_cpu

……
gdtr:base=0x5cb8, limit=0x7ff
idtr:base=0x54b8, limit=0x7ff
……
x /10 0x5cb8

[bochs]:
0x00005cb8        0>:    0x00000000      0x00000000      0x00000fff
0x00c09a00
0x00005cc8       16>:    0x00000fff      0x00c09200      0x00000000
0x00000000
0x00005cd8       32>:    0x00000000      0x00000000
       这个GDT表的值是由Head.s代码末尾234行开始的常数数据定义的:
_gdt:       .quad 0x0000000000000000  /* NULL descriptor */
       .quad 0x<CHMETCNV w:st="on" unitname="C" sourcevalue="0" ha

posted on 2011-07-25 21:30  不知道  阅读(488)  评论(0)    收藏  举报

导航