再次解释,如果你没了解过保护模式,后续的一些名词和概念你可能不知所以然。
从这里开始,后面所使用的汇编都是 32 bit 的 AT&T 汇编
如果有兴趣了解 AT&T 汇编语法,请参考 x86 Assembly Guide
我没有找到比较好的文档,如果你知道,请推荐给我。
以下汇编注释,都是笔者遇到不懂,进行查阅并注释,并未非常详细的进行注释。
我的书写顺序可能有点奇怪,不是按照代码的编写顺序来完成的,而是按照代码的执行顺序来的。
head.s head
的源码为boot/head.s
文件。
查看源码中可配合反汇编提高效率:
1 2 3 4 as --32 -o boot/head.o boot/head.s objdump -m i386 -d boot/head.o
下面为源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 pg_dir: #注意这个标号,实际上代表着地址为0 的内存,下文中建立页目录的时候会用到。 .globl startup_32 startup_32: movl $0x10, %eax mov %ax, %ds mov %ax, %es mov %ax, %fs mov %ax, %gs lss stack_start, %esp #前面部分使用设置段寄存器中选择子为 0x10,即使用 GDT 中索引号为 2 的描述符(其实就是之前定义第二个段描述符) #stack_start 符号并没有定义 head.s 中,而是定义在 kernel/sched.c 中,见以下代码 #lss 指令为低32位加载到 esp #高16位加载到 SS #最终生成的机器码为反汇编为 ds:0x000252c0 #这里也了产生疑问,如何安排能够找到 kernel/sched.c 的符号的? #下面也可以看到 stack_start 对应结构体如下
1 2 3 4 5 6 struct { long * a; short b; } stack_start = { & user_stack [PAGE_SIZE>>2 ] , 0x10 };
疑问中。
1 2 call setup_idt # 设置中断描述符表 call setup_gdt # 设置全局描述符表
查看setup_idt
例程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 setup_idt: lea ignore_int, %edx movl $0x00080000, %eax movw %dx, %ax movw $0x8E00, %dx lea idt, %edi # idt是中断描述符表的地址 mov $256, %ecx #实际上这里在构造 中断门描述符 #首先将 标号 ignore_int 处的偏移地址放入 EDX #0x00080000 置入 EAX #将偏移地址的低16位置入 AX,现在EAX 的值为 0x0008|偏移地址低16位(中断门描述符低32位) #目标代码段选择子为 0x08 ,描述符索引为 1 ,即为 前面定义的代码段 #0x8E00置入 DX ,现在EDX 的值为 偏移地址高16位|0x8e00(中断门描述符高32位) rp_sidt: movl %eax, (%edi) movl %edx, 4(%edi) addl $8, %edi dec %ecx jne rp_sidt lidt idt_descr # 加载中断描述符表寄存器值 ret #将中断门描述符填入中断描述符表所在的内存 #填入中断描述符表,一共256次 #最后 ret 至调用处 #最后的结果是所有的中断描述符最终都指向 ignore_int 例程 ...... .align 4 ignore_int: pushl %eax pushl %ecx #这部分略 ...... .align 8 #对齐为8字节 idt: .fill 256, 8, 0 # idt is uninitialized #该指令格式为 .fill repeat , size , value #重复次数,重复的大小,重复的值 #即这里为重复256次的 0x00,每个0x00 8字节 #这里为中断描述符表预留空间 ...... .word 0 idt_descr: .word 256 * 8 - 1 # idt contains 256 entries .long idt #前 2 字节是描述符的段界限(段长度-1) #后 4 字节是中断描述符表的内存地址
实际上 中断门描述符表的 256 项均已设置,这里仅展示前2个。所以的中断门描述符指向同一中断。
查看setup_gdt
例程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 setup_gdt: lgdt gdt_descr ret ...... .word 0 gdt_descr: .word 256 * 8 - 1 # so does gdt (not that that's any .long gdt # magic number, but it works for me :^) ...... 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 */ #很容易猜到 .quad 是伪指令 声明了 8字节(64位) #定义了4个描述符 #第一个描述符为空(null) #---------------------------------------------------------------- #第二个描述符(索引号为 0x08) #段基地址 0x0000 0000 #粒度为 1 段界限单位为 4KB 默认操作数大小为 32位 # TYPE 1010 代码段 可执行、可读 #段界限为 0x0 0fff 实际使用的段界限为 0xFF FFFF 实际上这段空间即为 16MB #---------------------------------------------------------------- #第三个描述符(索引号为 0x10) #段基地址 0x0000 0000 #粒度为 1 段界限单位为 4KB 默认操作数大小为 32位 #TYPE 0010 数据段:可读、可写 #段界限为 0x0 0fff 实际使用的段界限为 0xFF FFFF 实际上这段空间即为 16MB #--------------------------------------------------------------- #第四个描述符(索引号为 0x18) 为空 #其余 252 个描述符使用 0 填充。为其余的段描述符预留空间
经过例程setup_idt
和setup_gdt
重新设置了中断描述符表和全局描述符表,虽然段寄存器的选择子目前指向正确,但是描述符高速缓存器 加载的是 setup.s
执行时设置段描述符,需要重新加载选择子,刷新描述符高速缓存出去,使其指向正确的地址。
为什么要重新设置 GDT ?
原来的 GDT 指向的是 setup.s
中在内存中的位置,这个位置现在被system
模块覆盖,所以需要重新设置GDT 。
1 2 3 4 5 6 7 8 9 10 11 movl $0x10, %eax # reload all the segment registers mov %ax, %ds # after changing gdt. CS was already mov %ax, %es # reloaded in 'setup_gdt' mov %ax, %fs mov %ax, %gs lss stack_start, %esp #这里的注释说 CS 在setup_gdt 加载过了 #从前面可以看到,例程 setup_gdt 其实非常简单 ,并没有涉及 CS 的加载。 #笔者认为这里是个错误,但是不影响接下来的执行。 #其余的指令为使用 0x10 加载到各个段寄存器,刷新描述符高速缓存器 #最后的 stack_start 和前面同理
测试 A20 gate 是否开启:
1 2 3 4 5 6 7 8 xorl %eax, %eax 1: incl %eax # check that A20 really IS enabled movl %eax, 0x000000 # loop forever if it isn't cmpl %eax, 0x100000 je 1b #向 EAX 中写入 1,然后查看地址 0x0:0x100000 处是否也是相同的值 #如果相同,表示A20 gate 未开启,产生了地址回绕,无法访问 1MB 以上的内存 #如果不同,则表示 A20 gate 正确开启
检查是否存在x87
协处理器:
协处理器 、 FPU 、浮点运算单元 都是同一个东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 movl %cr0, %eax # check math chip andl $0x80000011, %eax # Save PG,PE,ET /* "orl $0x10020,%eax" here for 486 might be good */ orl $2, %eax # set MP movl %eax, %cr0 call check_x87 jmp after_page_tables #这里的做法是,假设存在协处理器,执行协处理器指令,检查标志位,如果出错则表示协处理器不存在。 #和 0x80000011 进行与操作,位 31 、 位 1 、位 0 将置1 ,这几位分别代表PG(分页)、ET( 为 1 表示系统有 80387协处理器,为 0 表示使用 80287 协处理器)、PE(保护模式) #注意,是与操作,如果 CR0 中本来未开启,则不会置1 #将位1 置 1 (MP) #设置 CR0 的 PG(分页)、ET( 80387协处理器位)、PE(保护模式) #注释,是与操作,如果 CRO 对应的位不存在,则不会置1 。 #最后设置 MP 位(位1 的 协处理器存在位) check_x87: fninit fstsw %ax cmpb $0, %al # je 1f /* no coprocessor: have to set bits */ movl %cr0, %eax xorl $6, %eax /* reset MP, set EM */ movl %eax, %cr0 ret #fninit 指令 为初始化协处理器指令,其余作用略,重点是将清除状态字(全部为0) # fstsw %ax 指令将状态字存储到 AX中 #如果状态字为 0 ,表示存在 X87 协处理器 ,往下jmp 至 标号 1处 #如果状态字为其他,表示不存在 x97 协处理器 #将 CR0 EM(位2,协处理器仿真位)置 1、MP(位1 ,协处理器存在标志位) 置0 。 .align 4 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ ret #DBE4 实际上是 fsetpm 指令的机器码,该指令为协处理器的指令 #该指令为 设置 80287协处理器 在保护模式下,80387 协处理器将该指令视为空指令
开始准备初始化页目录表和页表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 .org 0x1000 pg0: .org 0x2000 pg1: .org 0x3000 pg2: .org 0x4000 pg3: .org 0x5000 .org 0x5000 tmp_floppy_area: .fill 1024,1,0 #1024 次 1字节,填充内容为 0x0 # org 指令表示该指令的起始地址为 0x5000 #这几个标号留待下文使用 after_page_tables: pushl $0 # These are the parameters to main :-) pushl $0 pushl $0 pushl $L6 # return address for main, if it decides to. pushl $main jmp setup_paging # L6: jmp L6 # main should never return here, but # just in case, we know what happens. #push 了 5个双字到栈中 #前三个为 0x0000 0000 #这里连续 push 了三次 0x00 #为了模拟 main 函数的 envp、argv 指针和 argc 参数 #最后push L6 ,实际上不允许 main ret ,如果 ret 回来将陷入 L6的死循环。
开始创建分页机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 .align 4 setup_paging: movl $1024 * 5, %ecx /* 5 pages - pg_dir+4 page tables */ xorl %eax, %eax xorl %edi, %edi /* pg_dir is at 0x000 */ cld;rep;stosl #从 0x0000 0000 到 0x0000 5000(不包括最后一个地址) ,填充 1024*5 次 的 0x0000 0000 #注意下面的标号 #这段空间大概为 20KB,这段空间的第一个 4KB 用作页目录表,后 16 KB 用作页表。 #为什么能够安全填充前 20KB ? #前面的tmp_floppy_ar 的起始地址就为 0x5000 ,也就是tmp_floppy_area 后面的代码实际上都在 地址 0000 之后,所以才能够随意的填充(当前也会覆盖前面的代码) #16KB 的页表项能够映射多少内存空间? #每个页表项占 4 个byte(字节) ,即最多有 4096 页表项,每个页表项表示的物理内存是 4kb #也就是说最多 16 KB 的页表项最多能寻址 16 MB 的内存。 movl $pg0 + 7, pg_dir /* set present bit/user r/w */ movl $pg1 + 7, pg_dir + 4 /* --------- " " --------- */ movl $pg2 + 7, pg_dir + 8 /* --------- " " --------- */ movl $pg3 + 7, pg_dir + 12 /* --------- " " --------- */ #pg_dir其实为就是 head.s 开始的第一行 #创建了4个页目录项 ,为什么都加了7?原因是低12位为页目录项的一些属性位 #这里页目录属性为7 ,表示 页存在、可读可写、允许所有特权级别的程序访问。 movl $pg3 + 4092, %edi movl $0xfff007, %eax std 1: stosl subl $0x1000, %eax jge 1b cld #std 设置增长方向为负,开始逆序填写页表 #$pg3 + 4092 是第4个页表的最后一页表项,对应的值为 0xfff007 ,实际上该地址是16 MB 的最后一个4kb起始地址 #每次填写的页表项 -0x1000 ,实际为 4kb的内存空间 #最终将16KB 的页表项均填写完成,设置增长方向为正。 xorl %eax, %eax /* pg_dir is at 0x0000 */ movl %eax, %cr3 /* cr3 - page directory start */ #开启页机制,CR3 控制寄存器中 存放着当前任务页目录的物理地址(我这里提到了任务,请忽略,后面会解释) #当前页目录的的物理地址为 0x0000 0000 #强调,CR3 控制寄存器、页目录项、页表项存放的都是物理地址(忽略页属性) movl %cr0, %eax orl $0x80000000, %eax movl %eax, %cr0 /* set paging (PG) bit */ ret /* this also flushes prefetch-queue */ #将CR0 的位 31 置1 ,开始分页模式 #注意,开启即生效,此时 段部件合成的地址为 线性地址 #线性地址经过页部件合成之后才是 物理地址
举例说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 b 0x54a5
最后是一个ret
结尾,前面pushl $main
将main
符号的偏移地址 push
到栈中,这里到ret
将该地址pop
到ip
中。至此,head.s
结束,控制转移给 init/main.c
中的main
函数。
一些汇编指令的解释 关于 .align
1 2 3 4 5 #tmp.asm .data .byte 1 .byte 0x11 #只有 .data 段有数据,其他为空
1 2 as --32 -o a.o tmp.asm objdump -s -j .text a.o
可以看到 0x11
紧接着 0x01
,0x01
的地址为 0x00
,0x11
的对称为0x11
。
使用伪指令:
1 2 3 4 5 #tmp.asm .data .byte 1 .align 2 .byte 0x11
可以看到0x11
的偏移为 0x02
。
1 2 3 4 5 #tmp.asm .data .byte 1 .align 4 .byte 0x11
可以看到0x11
的偏移为 0x04
。
1 2 3 4 5 #tmp.asm .data .byte 1 .align 16 .byte 0x11
可以看到0x11
的偏移为 0x10
。
即.align n
,下一个数据 起始地址为地址%n=0
的地址处。
小结 总结一下 目前 head.s
完成的功能:
设置 IDT 、GDT;
测试 A20 gate ;
检查是否存在 x87
协处理器;
开启分页机制,包括创建页目录表,创建页表。
最后控制转移至 init/main.c
处。
总结下目前的疑问
这里使用 main
符号,实现了 汇编和 C 的相互调用,明天在看下这部分;
汇编调用了 C 中的结构体stack_start
,需要理解这里的栈式如何确定的;
在bootsect
对设备号的疑问仍然存在;
如何精确的安排hdad.s
在system 模块头部,或者说如何编译组织的 system
模块。
参考
LInux 内核 完全注释
x86汇编语言:从实模式到保护模式(有点忘了,又去翻书)