加载中…
个人资料
老徐
老徐
  • 博客等级:
  • 博客积分:0
  • 博客访问:822,939
  • 关注人气:156
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
相关博文
推荐博文
谁看过这篇博文
加载中…
正文 字体大小:

[Linux]由linux0.11代码小窥内存分段机制

(2013-01-23 14:49:41)
标签:

0.11

分段

实模式

保护模式

it

分类: Linux/Shell
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文件,将其中所有内容改为:
"D:\userdata\qingfxu\Bochs-2.1.1\bochsdbg.exe" -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表
       在保护模式下,段寄存器所存储的将是段描述符表的某个索引值,索引值指定的段描述符项中含有需要寻址的内存段的基地址、段的最大长度值和段的访问级别等信息。计算线性地址的示意图如下:

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

        LLDT指令与LGDT指令不同的是,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值——这一点和刚才所 讨论的通过段积存器引用段的模式是一样的。

       GDT是Protected Mode所必须的数据结构,那么我们在进入Protected Mode之前,必须设定好GDT,并通过LGDT将其装入相应的寄存器。尽管GDT允许被放在内存的任何位置,但由于GDT中的元素——描述符——都是64-bit长,也就是说都是8个字节,所以为了让CPU对GDT的访问速度达到最快,我们应该将GDT的入口地址放在以8个字节对齐,也就是说是8的倍数的地址位置。GDT中第一个描述符必须是一个空描述符,也就是它的内容应该全部为0。如果引用这个描述符进行内存访问,则是产生General Protection异常。

在这里,我们只是处于Booting阶段,所以我们只需要初步设置一下GDT,等真正进入Protected Mode,启动了OS Kernel之后,具体OS打算如何设置GDT,使用何种内存管理模式,由Kernel自身来设置,Booting只需要给Kernel的数据段和代码段 设置全部线性空间就可以了。
下面就是在Booting阶段为进入Protected Mode而设置的临时的gdt。这里定义了3个段描述符:第一个是系统规定的空描述符,第2个是引用4 GB线性空间的代码段,第3个是引用4 GB线性空间的数据段。这是"Basic Flat Model"所要求的最下GDT设置,但就booting阶段,只是为了进入Protected Mode,并为内核提供一个连续的,最大的线性空间这个目的而言,已经足够了。

# Descriptor tables
gdt:
.word 0, 0, 0, 0 # dummy
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)

设置好GDT之后,我们需要通过LGDT指令将设定的gdt的入口地址和gdt表的大小装入GDTR寄存器。
GDTR 寄存器包括两部分:32-bit的线性基地址,以及16-bit的GDT大小(以字节为单位)。需要注意的是,对于32-bit线性基地址,必须是32- bit绝对物理地址,而不是相对于某个段的偏移量。而我们在Booting阶段,在进入Protected Mode之前,我们CS和DS设置很可能不是0,所以我们必须计算出gdt的绝对物理地址。

为了执行LGDT指令,你需要把这两部分内容放在内存的某个位置,然后将这个位置的内存地址作为操作数传递给LGDT指令。然后LGDT指令会自动将保存在这个位置的这两部分值装入GDTR寄存器。
# 这是存放GDTR所需的两部分内容的位置
gdt_48:
.word 0x8000 # gdt limit=2048,
# 256 GDT entries
.word 0, 0 # gdt base (filled in later)
# 下面这段代码用来计算GDT的32-bit线性地址,并将其装入GDTR寄存器。
xorl �x, �x # Compute gdt_base
movw %ds, %ax # (Convert %ds:gdt to a linear ptr)
shll , �x
addl $gdt, �x
movl �x, (gdt_48+2)
lgdt gdt_48 # load gdt with whatever is appropriate

IDTR的格式和GDTR相同,IDTR的装入方式和GDTR也相同。由于IDT中相关的中断处理程序需要让OS Kernel来设定,所以在Booting阶段,我们只需要将IDTR中IDT的基地址和Size都设为0就可以了,随后,等进入Protected Mode之后,由OS Kernel来真正设置它。
进入Protected Mode,还是进入Real Mode,完全靠CR0寄存器的PE标志位来控制:如果PE=1,则CPU切换到PM,否则,则进入RM。

设置CR0-PE位的方法有两种:
第一种是80286所使用的LMSW指令,后来的80386及更高型号的CPU为了保持向后兼容,都保留了这个指令。这个指令只能影响最低的4 bit,即PE,MP,EM和TS,对其它的没有影响。
#
#通过LMSW指令进入Protected Mode
#
movw , %ax # protected mode (PE) bit
lmsw %ax # This is it!

第二种是Intel所建议的在80386以后的CPU上使用的进入PM的方式,即通过MOV指令。MOV指令可以设置CR0寄存器的所有域的值。
#
#通过MOV指令进入Protected Mode
#
movl %cr0, �x
xorb , %al # set PE = 1
movl �x, %cr0 # go!!
OK,现在已经进入Protected Mode了。
很简单,right?But It's not over yet!


       因此,在进入保护模式之前,需要建立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  
       .quad 0x

0

阅读 评论 收藏 转载 喜欢 打印举报/Report
  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 电话:4000520066 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有