2009年(16)
分类: LINUX
2009-11-30 22:10:27
阅读本文手头上应该有一份,引导程序调试软件(其实是个虚拟机,不过它的调试功能实在是完美)和配套的linux0.11内核img(linux-0.11-devel-040329.zip)。最好再有一本代码注释,推荐赵炯博士的《》。显然,bochs的使用方法必须知道,具体操作请参阅《Linux内核完全注释》第14章;在bochs能够正确运行之后,使用bochsdbg进行调试,其使用方法见“”。要想很好的理解操作系统,应具备一定的底层知识,推荐《深入理解计算机系统》。如果对老版本的linux很有兴趣,建议去,感谢赵炯博士的无私奉献。
下载linux-0.11-devel-040329.zip,解压缩到bochs的安装目录,其中包含一个bochs
12> romimage: file=..\BIOS-bochs-latest, address=0xf0000
编辑run.bat文件,将其中所有内容改为:
"..\bochsdbg" -q -f bochsrc-Hd.bxrc
运行run.bat,即启动调试工具bochsdbg。
在0x0000:0x
(0) Breakpoint 1, 0x
Next at t=16252460
(0) [0x
0x0000:0x
引导程序一开始将其自身代码从0x
[bochs]:
0x
0x
0x
0xd08ec08e
f
(0) Breakpoint 2, 0x
Next at t=16252723
(0) [0x
[bochs]:
0x00090000
0x
0x00090010
0xd08ec08e
在反汇编代码中看到jmp far 9000:0018这一行,通过调试可看到其实际效果:将cs段设置为0x9000,从偏移量0x18处开始执行,也就是设置eip为0x18。命令行如下:
……
eip 0x
eflags 0x246 582
cs 0x0 0
……
Next at t=16252724
(0) [0x00090018] 9000:0018 (unk. ctxt): mov ax, cs ; 8cc8
……
eip 0x18 0x18
eflags 0x246 582
cs 0x9000 36864
……
红色标记的cs和eip组合起来的值cs:eip即指向实模式下的代码逻辑位置。同样通过cs<<4 + eip来计算出实际地址。
在保护模式下,段寄存器所存储的将是段描述符表的某个索引值,索引值指定的段描述符项中含有需要寻址的内存段的基地址、段的最大长度值和段的访问级别等信息。计算线性地址的示意图如下:
图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的指令位置,继续调试,命令行如下:
(0) Breakpoint 3, 0x
Next at t=16483610
(0) [0x00090200] 9020:0000 (unk. ctxt): mov ax, 0x9000 ; b80090
……
0009029d: ( ): lidt ds:0x
……
(0) Breakpoint 4, 0x
Next at t=16750806
(0) [0x
反汇编代码lgdt ds:0x132表明将ds:0x132所在位置的数据赋给gdtr,lgdt总共需要6个字节,其中两个字节为GDT表的长度,另外4个字节表明GDT表的基址。通过调试可以看到这条指令的实际作用,命令行如下:
……
ds 0x9020 36896
……
[bochs]:
0x00090332
0x00000000
Next at t=16750807
(0) [0x
……
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表,命令行如下:
[bochs]:
0x00090314
0x
0x00090324
0x08000000
0x00090334
按照一个描述符8字节长度整理一下得:
0x00000000 0x00000000 ! dummy
0x000007ff 0x
0x000007ff 0x
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这一行代码所在位置,命令行如下:
……
000902fe: ( ): mov ax, 0x1 ; b80100
00090301: ( ): lmsw ax ;
00090304: ( ): jmp far 0008:0000 ; ea00000800
……
jmp far 0008:0000指令的实际效果是设置cs为0x0008,设置eip为0x0000,这里的0x0008即为保护模式下的段选择符,写成二进制形式0000000000001000,前两位00表示特权级0,第三位0表示该选择符用于选择全局描述符表,高13位0000000000001表示使用全局描述符的第一项,即前面提到的内核代码段选择符:0x00007fff 0x
(0) Breakpoint 5, 0x
Next at t=16750869
(0) [0x00090304] 9020:00000104 (unk. ctxt): jmp far 0008:0000 ; ea000008
00
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 0x
Head.s一开始重新设置IDT表和GDT表,建立方法和在Setup.s中相差不大,下面来看看重新建立的GDT表。命令行如下:
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:0x
00000014: ( ): call .+0x
00000019: ( ): call .+0x
0000001e: ( ): mov eax, 0x10 ; b810000000
00000023: ( ): mov ds, ax ; 8ed8
(0) Breakpoint 6, 0x1e in ?? ()
Next at t=16752168
(0) [0x0000001e] 0008:0000001e (unk. ctxt): mov eax, 0x10 ; b8100000
00
……
gdtr:base=0x5cb8, limit=0x7ff
idtr:base=0x54b8, limit=0x7ff
……
[bochs]:
0x00005cb8
0x
0x00005cc8
0x00000000
0x00005cd8
这个GDT表的值是由Head.s代码末尾234行开始的常数数据定义的:
_gdt: .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x
.quad 0x
.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”,事实上以调试的方法来辅助阅读是相当有益的