文章目录代码流程解析第一阶段MBR (主引导记录) 执行第二阶段加载器 (Loader) 执行第三阶段内核 (Kernel) 执行jmp KERNEL_ENTRY_POINT 重点详解总结代码代码引自《操作系统真象还原》第五章第三节加载内核下面四段代码共同构成了一个从零开始、功能完整的简易操作系统引导链。它们协同工作将系统从加电状态一步步引导至内核运行。这是一个经典的 x86 保护模式启动流程。;------------- loader和kernel ---------- LOADER_BASE_ADDR equ 0x900 LOADER_START_SECTOR equ 0x2 LOADER_STACK_TOP equ LOADER_BASE_ADDR ;这一条之前是在loader.S中定义现在搬过来了 KERNEL_BIN_BASE_ADDR equ 0x70000 ;定义内核在内存中的缓冲区也就是将编译好的内核文件暂时存储在内存中的位置 KERNEL_START_SECTOR equ 0x9 ;定义内核在磁盘的起始扇区 KERNEL_ENTRY_POINT equ 0xc0001500 ;定义内核可执行代码的入口地址 PAGE_DIR_TABLE_POS equ 0x100000 ;页目录表在内存中的起始位置——从1M开始的位置 ;-------------- 模块化的gdt描述符字段宏------------- DESC_G_4K equ 1_00000000000000000000000b ;设置段界限的单位为4KB DESC_D_32 equ 1_0000000000000000000000b ;设置代码段/数据段的有效地址段内偏移地址及操作数大小为32位而非16位 DESC_L equ 0_000000000000000000000b ;64位代码段标记位我们现在是在编写32位操作系统此处标记为0便可。 DESC_AVL equ 0_00000000000000000000b ;此标志位是为了给操作系统或其他软件设计的一个自定义位 ;可以将这个位用于任何自定义的需求。 ;比如操作系统可以用这个位来标记这个段是否正在被使用或者用于其他特定的需求。 ;这取决于开发者如何使用这个位。但从硬件的角度来看AVL位没有任何特定的功能或意义它的使用完全由软件决定。 DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;定义代码段要用的段描述符高32位中16~19段界限为全1 DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;定义数据段要用的段描述符高32位中16~19段界限为全1 DESC_LIMIT_VIDEO2 equ 0000_000000000000000b ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0 DESC_P equ 1_000000000000000b ;定义了段描述符中的P标志位表示该段描述符指向的段是否在内存中 DESC_DPL_0 equ 00_0000000000000b ;定义DPL为0的字段 DESC_DPL_1 equ 01_0000000000000b ;定义DPL为1的字段 DESC_DPL_2 equ 10_0000000000000b ;定义DPL为2的字段 DESC_DPL_3 equ 11_0000000000000b ;定义DPL为3的字段 DESC_S_CODE equ 1_000000000000b ;无论代码段还是数据段对于cpu来说都是非系统段所以将S位置为1见书p153图 DESC_S_DATA equ DESC_S_CODE ;无论代码段还是数据段对于cpu来说都是非系统段所以将S位置为1见书p153图 DESC_S_sys equ 0_000000000000b ;将段描述符的S位置为0表示系统段 DESC_TYPE_CODE equ 1000_00000000b ;x1,c0,r0,a0 代码段是可执行的,非依从的,不可读的,已访问位a清0. DESC_TYPE_DATA equ 0010_00000000b ;x0,e0,w1,a0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0. ;定义代码段数据段显存段的高32位 DESC_CODE_HIGH4 equ (0x00 24) DESC_G_4K DESC_D_32 DESC_L DESC_AVL DESC_LIMIT_CODE2 DESC_P DESC_DPL_0 DESC_S_CODE DESC_TYPE_CODE 0x00 DESC_DATA_HIGH4 equ (0x00 24) DESC_G_4K DESC_D_32 DESC_L DESC_AVL DESC_LIMIT_DATA2 DESC_P DESC_DPL_0 DESC_S_DATA DESC_TYPE_DATA 0x00 DESC_VIDEO_HIGH4 equ (0x00 24) DESC_G_4K DESC_D_32 DESC_L DESC_AVL DESC_LIMIT_VIDEO2 DESC_P DESC_DPL_0 DESC_S_DATA DESC_TYPE_DATA 0x0b ;-------------- 模块化的选择子字段宏 --------------- RPL0 equ 00b ;定义选择字的RPL为0 RPL1 equ 01b ;定义选择子的RPL为1 RPL2 equ 10b ;定义选择字的RPL为2 RPL3 equ 11b ;定义选择子的RPL为3 TI_GDT equ 000b ;定义段选择子请求的段描述符是在GDT中 TI_LDT equ 100b ;定义段选择子请求的段描述符是在LDT中 ;---------模块化的页目录表字段,PWT PCD A D G AVL 暂时不用设置 ---------- PG_P equ 1b PG_RW_R equ 00b PG_RW_W equ 10b PG_US_S equ 000b PG_US_U equ 100b ;------------- 程序段的 type 定义 -------------- PT_NULL equ 0;主引导程序 ;------------------------------------------------------------ %include boot.inc SECTION MBR vstart0x7c00 mov ax,cs mov ds,ax mov es,ax mov ss,ax mov fs,ax mov sp,0x7c00 mov ax,0xb800 mov gs,ax ; 清屏 ;利用0x06号功能上卷全部行则可清屏。 ; ----------------------------------------------------------- ;INT 0x10 功能号:0x06 功能描述:上卷窗口 ;------------------------------------------------------ ;输入 ;AH 功能号 0x06 ;AL 上卷的行数(如果为0,表示全部) ;BH 上卷行属性 ;(CL,CH) 窗口左上角的(X,Y)位置 ;(DL,DH) 窗口右下角的(X,Y)位置 ;无返回值 mov ax, 0600h mov bx, 0700h mov cx, 0 ; 左上角: (0, 0) mov dx, 184fh ; 右下角: (80,25), ; 因为VGA文本模式中一行只能容纳80个字符,共25行。 ; 下标从0开始所以0x1824,0x4f79 int 10h ; int 10h ; 输出字符串:MBR mov byte [gs:0x00],1 mov byte [gs:0x01],0xA4 mov byte [gs:0x02], mov byte [gs:0x03],0xA4 mov byte [gs:0x04],M mov byte [gs:0x05],0xA4 ;A表示绿色背景闪烁4表示前景色为红色 mov byte [gs:0x06],B mov byte [gs:0x07],0xA4 mov byte [gs:0x08],R mov byte [gs:0x09],0xA4 mov eax,LOADER_START_SECTOR ; 起始扇区lba地址 mov bx,LOADER_BASE_ADDR ; 写入的地址 mov cx,4 ; 待读入的扇区数 call rd_disk_m_16 ; 以下读取程序的起始部分一个扇区 jmp LOADER_BASE_ADDR 0x300 ;------------------------------------------------------------------------------- ;功能:读取硬盘n个扇区 rd_disk_m_16: ;------------------------------------------------------------------------------- ; eaxLBA扇区号 ; ebx将数据写入的内存地址 ; ecx读入的扇区数 mov esi,eax ;备份eax mov di,cx ;备份cx ;读写硬盘: ;第1步选择特定通道的寄存器设置要读取的扇区数 mov dx,0x1f2 mov al,cl out dx,al ;读取的扇区数 mov eax,esi ;恢复ax ;第2步在特定通道寄存器中放入要读取扇区的地址将LBA地址存入0x1f3 ~ 0x1f6 ;LBA地址7~0位写入端口0x1f3 mov dx,0x1f3 out dx,al ;LBA地址15~8位写入端口0x1f4 mov cl,8 shr eax,cl mov dx,0x1f4 out dx,al ;LBA地址23~16位写入端口0x1f5 shr eax,cl mov dx,0x1f5 out dx,al shr eax,cl and al,0x0f ;lba第24~27位 or al,0xe0 ; 设置74位为1110,表示lba模式 mov dx,0x1f6 out dx,al ;第3步向0x1f7端口写入读命令0x20 mov dx,0x1f7 mov al,0x20 out dx,al ;第4步检测硬盘状态 .not_ready: ;同一端口写时表示写入命令字读时表示读入硬盘状态 nop in al,dx and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输第7位为1表示硬盘忙 cmp al,0x08 jnz .not_ready ;若未准备好继续等。 ;第5步从0x1f0端口读数据 mov ax, di ;di当中存储的是要读取的扇区数 mov dx, 256 ;每个扇区512字节一次读取两个字节所以一个扇区就要读取256次与扇区数相乘就等得到总读取次数 mul dx ;8位乘法与16位乘法知识查看书p133,注意16位乘法会改变dx的值 mov cx, ax ; 得到了要读取的总次数然后将这个数字放入cx中 mov dx, 0x1f0 .go_on_read: in ax,dx mov [bx],ax add bx,2 loop .go_on_read ret times 510-($-$$) db 0 db 0x55,0xaa%include boot.inc section loader vstartLOADER_BASE_ADDR GDT_BASE: ;构建gdt及其内部的描述符 dd 0x00000000 dd 0x00000000 CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4 DATA_STACK_DESC: dd 0x0000FFFF dd DESC_DATA_HIGH4 VIDEO_DESC: dd 0x80000007 ;limit(0xbffff-0xb8000)/4k0x7 dd DESC_VIDEO_HIGH4 ; 此时dpl已改为0 GDT_SIZE equ $ - GDT_BASE GDT_LIMIT equ GDT_SIZE - 1 times 60 dq 0 ; 此处预留60个描述符的空间 SELECTOR_CODE equ (0x00013) TI_GDT RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 TI_GDT RPL0 SELECTOR_DATA equ (0x00023) TI_GDT RPL0 ; 同上 SELECTOR_VIDEO equ (0x00033) TI_GDT RPL0 ; 同上 total_mem_bytes dd 0 ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。 ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900, ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址 gdt_ptr dw GDT_LIMIT ;定义加载进入GDTR的数据前2字节是gdt界限后4字节是gdt起始地址 dd GDT_BASE ards_buf times 244 db 0 ;人工对齐:total_mem_bytes4字节gdt_ptr6字节ards_buf244字节ards_nr2,共256字节 ards_nr dw 0 ;用于记录ards结构体数量 loader_start: ;------- int 15h eax 0000E820h ,edx 534D4150h (SMAP) 获取内存布局 ------- xor ebx, ebx ;第一次调用时ebx值要为0 mov edx, 0x534d4150 ;edx只赋值一次循环体中不会改变 mov di, ards_buf ;ards结构缓冲区 .e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构 mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。 mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节 int 0x15 add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置 inc word [ards_nr] ;记录ARDS数量 cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回当前已是最后一个 jnz .e820_mem_get_loop ;在所有ards结构中找出(base_add_low length_low)的最大值即内存的容量。 mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量 mov ebx, ards_buf xor edx, edx ;edx为最大的内存容量,在此先清0 .find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用 mov eax, [ebx] ;base_add_low add eax, [ebx8] ;length_low add ebx, 20 ;指向缓冲区中下一个ARDS结构 cmp edx, eax ;冒泡排序找出最大,edx寄存器始终是最大的内存容量 jge .next_ards mov edx, eax ;edx为总内存大小 .next_ards: loop .find_max_mem_area mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。 ;----------------- 准备进入保护模式 ------------------------------------------ ;1 打开A20 ;2 加载gdt ;3 将cr0的pe位置1 ;----------------- 打开A20 ---------------- in al, 0x92 or al, 0000_0010B out 0x92,al ;----------------- 加载GDT ---------------- lgdt [gdt_ptr] ;----------------- cr0第0位置1 ---------------- mov eax,cr0 or eax,0x00000001 mov cr0,eax ;jmp dword SELECTOR_CODE:p_mode_start jmp SELECTOR_CODE:p_mode_start ; 刷新流水线避免分支预测的影响,这种cpu优化策略最怕jmp跳转 ; 这将导致之前做的预测失效从而起到了刷新的作用。 .error_hlt: ;出错则挂起 hlt [bits 32] p_mode_start: mov ax,SELECTOR_DATA mov ds,ax mov es,ax mov ss,ax mov esp,LOADER_STACK_TOP mov ax,SELECTOR_VIDEO mov gs,ax ; ------------------------- 加载kernel ---------------------- mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇区号 mov ebx, KERNEL_BIN_BASE_ADDR ; 从磁盘读出后写入到ebx指定的地址 mov ecx, 200 ; 读入的扇区数 call rd_disk_m_32 call setup_page ;创建页目录表的函数,我们的页目录表必须放在1M开始的位置所以必须在开启保护模式后运行 ;以下两句是将gdt描述符中视频段描述符中的段基址0xc0000000 mov ebx, [gdt_ptr 2] ;ebx中存着GDT_BASE or dword [ebx 0x18 4], 0xc0000000 ;视频段是第3个段描述符,每个描述符是8字节,故0x18 24然后4是取出了视频段段描述符的高4字节。然后or操作段基址最高位c add dword [gdt_ptr 2], 0xc0000000 ;将gdt的基址加上0xc0000000使其成为内核所在的高地址 add esp, 0xc0000000 ; 将栈指针同样映射到内核地址 mov eax, PAGE_DIR_TABLE_POS ; 把页目录地址赋给cr3 mov cr3, eax mov eax, cr0 ; 打开cr0的pg位(第31位) or eax, 0x80000000 mov cr0, eax lgdt [gdt_ptr] ;在开启分页后,用gdt新的地址重新加载 enter_kernel: call kernel_init mov esp, 0xc009f000 jmp KERNEL_ENTRY_POINT ; 用地址0x1500访问测试结果ok ;----------------- 将kernel.bin中的segment拷贝到编译的地址 ----------- kernel_init: xor eax, eax ;清空eax xor ebx, ebx ;清空ebx, ebx记录程序头表地址 xor ecx, ecx ;清空ecx, cx记录程序头表中的program header数量 xor edx, edx ;清空edx, dx 记录program header尺寸 mov dx, [KERNEL_BIN_BASE_ADDR 42] ; 偏移文件42字节处的属性是e_phentsize,表示program header table中每个program header大小 mov ebx, [KERNEL_BIN_BASE_ADDR 28] ; 偏移文件开始部分28字节的地方是e_phoff,表示program header table的偏移ebx中是第1 个program header在文件中的偏移量 ; 其实该值是0x34,不过还是谨慎一点这里来读取实际值 add ebx, KERNEL_BIN_BASE_ADDR ; 现在ebx中存着第一个program header的内存地址 mov cx, [KERNEL_BIN_BASE_ADDR 44] ; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header .each_segment: cmp byte [ebx 0], PT_NULL ; 若p_type等于 PT_NULL,说明此program header未使用。 je .PTNULL ;为函数memcpy压入参数,参数是从右往左依然压入.函数原型类似于 memcpy(dst,src,size) push dword [ebx 16] ; program header中偏移16字节的地方是p_filesz,压入函数memcpy的第三个参数:size mov eax, [ebx 4] ; 距程序头偏移量为4字节的位置是p_offset该值是本program header 所表示的段相对于文件的偏移 add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址 push eax ; 压入函数memcpy的第二个参数:源地址 push dword [ebx 8] ; 压入函数memcpy的第一个参数:目的地址,偏移程序头8字节的位置是p_vaddr这就是目的地址 call mem_cpy ; 调用mem_cpy完成段复制 add esp,12 ; 清理栈中压入的三个参数 .PTNULL: add ebx, edx ; edx为program header大小,即e_phentsize,在此ebx指向下一个program header loop .each_segment ret ;---------- 逐字节拷贝 mem_cpy(dst,src,size) ------------ ;输入:栈中三个参数(dst,src,size) ;输出:无 ;--------------------------------------------------------- mem_cpy: cld ;将FLAG的方向标志位DF清零rep在执行循环时候sidi就会加1 push ebp ;这两句指令是在进行栈框架构建 mov ebp, esp push ecx ; rep指令用到了ecx但ecx对于外层段的循环还有用故先入栈备份 mov edi, [ebp 8] ; dstedi与esi作为偏移没有指定段寄存器的话默认是ss寄存器进行配合 mov esi, [ebp 12] ; src mov ecx, [ebp 16] ; size rep movsb ; 逐字节拷贝 ;恢复环境 pop ecx pop ebp ret setup_page: ;------------------------------------------ 创建页目录及页表 ------------------------------------- ;----------------以下6行是将1M开始的4KB置为0将页目录表初始化 mov ecx, 4096 ;创建4096个byte 0循环4096次 mov esi, 0 ;用esi来作为偏移量寻址 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS esi], 0 inc esi loop .clear_page_dir ; ----------------初始化页目录表让0号项与768号指向同一个页表该页表管理从0开始4M的空间 .create_pde: ;一个页目录表项可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表这是为将地址映射为内核地址做准备 mov eax, PAGE_DIR_TABLE_POS ; eax中存着页目录表的位置 add eax, 0x1000 ; 在页目录表位置的基础上4K页目录表的大小现在eax中第一个页表的起始位置 mov ebx, eax ; 此处为ebx赋值现在ebx存着第一个页表的起始位置 or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问. ; 现在eax中的值符合一个页目录项的要求了高20位是一个指向第一个页表的4K整数倍地址低12位是相关属性设置 mov [PAGE_DIR_TABLE_POS 0x0], eax ; 页目录表0号项写入第一个页表的位置(0x101000)及属性(7) mov [PAGE_DIR_TABLE_POS 0xc00], eax ; 页目录表768号项写入第一个页表的位置(0x101000)及属性(7) sub eax, 0x1000 ;----------------- 使最后一个目录项指向页目录表自己的地址为的是将来动态操作页表做准备 mov [PAGE_DIR_TABLE_POS 4092], eax ;属性包含PG_US_U是为了将来init进程运行在用户空间访问这个页目录表项 mov ecx, 256 ; -----------------初始化第一个页表因为我们的操作系统不会超过1M所以只用初始化256项 mov esi, 0 ; esi来做寻址页表项的偏移量 mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US1,RW1,P1 .create_pte: ; 创建Page Table Entry mov [ebxesi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 add edx,4096 ; edx指向下一个4kb空间且已经设定好了属性故edx中是一个完整指向下一个4kb物理空间的页表表项 inc esi ; 寻址页表项的偏移量1 loop .create_pte ;循环设定第一个页表的256项 ; -------------------初始化页目录表769号-1022号项769号项指向第二个页表的地址此页表紧挨着上面的第一个页表770号指向第三个以此类推 mov eax, PAGE_DIR_TABLE_POS ; eax存页目录表的起始位置 add eax, 0x2000 ; 此时eax为第二个页表的位置 or eax, PG_US_U | PG_RW_W | PG_P ; 设置页目录表项相关属性US,RW和P位都为1现在eax中的值是一个完整的指向第二个页表的页目录表项 mov ebx, PAGE_DIR_TABLE_POS ; ebx现在存着页目录表的起始位置 mov ecx, 254 ; 要设置254个表项 mov esi, 769 ; 要设置的页目录表项的偏移起始 .create_kernel_pde: mov [ebxesi*4], eax ; 设置页目录表项 inc esi ; 增加要设置的页目录表项的偏移 add eax, 0x1000 ; eax指向下一个页表的位置由于之前设定了属性所以eax是一个完整的指向下一个页表的页目录表项 loop .create_kernel_pde ; 循环设定254个页目录表项 ret ;------------------------------------------------------------------------------- ;功能:读取硬盘n个扇区 rd_disk_m_32: ;------------------------------------------------------------------------------- ; eaxLBA扇区号 ; ebx将数据写入的内存地址 ; ecx读入的扇区数 mov esi,eax ;备份eax mov di,cx ;备份cx ;读写硬盘: ;第1步选择特定通道的寄存器设置要读取的扇区数 mov dx,0x1f2 mov al,cl out dx,al ;读取的扇区数 mov eax,esi ;恢复ax ;第2步在特定通道寄存器中放入要读取扇区的地址将LBA地址存入0x1f3 ~ 0x1f6 ;LBA地址7~0位写入端口0x1f3 mov dx,0x1f3 out dx,al ;LBA地址15~8位写入端口0x1f4 mov cl,8 shr eax,cl mov dx,0x1f4 out dx,al ;LBA地址23~16位写入端口0x1f5 shr eax,cl mov dx,0x1f5 out dx,al shr eax,cl and al,0x0f ;lba第24~27位 or al,0xe0 ; 设置74位为1110,表示lba模式 mov dx,0x1f6 out dx,al ;第3步向0x1f7端口写入读命令0x20 mov dx,0x1f7 mov al,0x20 out dx,al ;第4步检测硬盘状态 .not_ready: ;同一端口写时表示写入命令字读时表示读入硬盘状态 nop in al,dx and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输第7位为1表示硬盘忙 cmp al,0x08 jnz .not_ready ;若未准备好继续等。 ;第5步从0x1f0端口读数据 mov ax, di ;di当中存储的是要读取的扇区数 mov dx, 256 ;每个扇区512字节一次读取两个字节所以一个扇区就要读取256次与扇区数相乘就等得到总读取次数 mul dx ;8位乘法与16位乘法知识查看书p133,注意16位乘法会改变dx的值 mov cx, ax ; 得到了要读取的总次数然后将这个数字放入cx中 mov dx, 0x1f0 .go_on_read: in ax,dx mov [ebx],ax ;与rd_disk_m_16相比就是把这两句的bx改成了ebx add ebx,2 ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。 ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位 ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时 ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖 ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址, ; 故程序出会错,不知道会跑到哪里去。 ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。 ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式. ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时, ; 也会认为要执行的指令是32位. ; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数 ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67 ; 临时改变当前cpu模式到另外的模式下. ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位. ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位. ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址 ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址. loop .go_on_read retint main(void){ while(1); return 0; }提示流程解析我们来梳理整个操作系统引导程序的完整执行顺序。这个过程从系统加电开始由 BIOS 启动最终将控制权交给内核。整个流程可以清晰地划分为几个阶段每个阶段都承担着特定的任务。第一阶段MBR (主引导记录) 执行这是启动流程的第一步由 BIOS 负责加载和执行。BIOS 加载 MBR:系统加电后BIOS 将硬盘的第一个扇区512字节加载到内存地址 0x7C00 处并跳转到该地址开始执行。初始化环境:设置段寄存器: 将 CS, DS, ES, SS, FS 都设置为 CS 的值确保所有段寄存器指向同一个基址。设置栈指针: 将 SP 设置为 0x7C00栈空间位于其下方。设置显存段寄存器: 将 GS 设置为 0xB800以便后续向显存写入字符。清屏与显示信息:调用 BIOS 中断 int 0x10功能号 0x06清空整个屏幕。直接向显存 gs:0x00 写入字符 ‘1’、’ 、‘M’、‘B’、‘R’ 及其属性使屏幕左上角显示 “1 MBR” 字样表明 MBR 正在运行。读取加载器 (Loader):调用子程序 rd_disk_m_16。从硬盘第 3 个扇区LBA 地址 0x2开始读取 4 个扇区的数据。将这些数据写入内存地址 0x900 处。跳转到加载器入口点:执行 jmp LOADER_BASE_ADDR 0x300即跳转到 0xC00 处将控制权移交给加载器程序。第二阶段加载器 (Loader) 执行加载器在实模式下开始执行其核心任务是完成向保护模式的过渡并为内核的加载做好准备。获取系统内存布局:调用 BIOS 中断 int 0x15子功能号 0xE820获取系统的内存布局信息。将返回的 ARDSAddress Range Descriptor Structure结构存储在 ards_buf 缓冲区中并记录数量 ards_nr。遍历所有 ARDS 结构计算每个内存区域的结束地址基地址 长度并找出其中最大的一个将其值存入 total_mem_bytes 变量作为系统的总内存容量。准备进入保护模式:打开 A20 地址线: 通过向端口 0x92 写入特定值启用 A20 地址线以访问超过 1MB 的内存。加载 GDT: 将预先定义好的全局描述符表GDT加载到 GDTR 寄存器。切换到保护模式: 设置 CR0 寄存器的 PE 位CPU 进入保护模式。刷新流水线: 执行一个长跳转指令 jmp SELECTOR_CODE:p_mode_start确保 CPU 在保护模式下正确执行后续指令。保护模式下的初始化:设置段寄存器: 将 DS, ES, SS 设置为数据段选择子 SELECTOR_DATA将 GS 设置为显存段选择子 SELECTOR_VIDEO。设置栈指针: 将 ESP 设置为 LOADER_STACK_TOP即 0x900。读取内核 (Kernel):调用子程序 rd_disk_m_32。从硬盘第 9 个扇区LBA 地址 0x9开始读取 200 个扇区的数据。将这些数据写入内存地址 0x70000 处。设置分页机制:调用子程序 setup_page创建页目录表和页表。建立虚拟地址到物理地址的映射关系将虚拟地址 0x00000000 到 0x003FFFFF 映射到物理地址 0x00000000。将虚拟地址 0xC0000000 到 0xC03FFFFF 也映射到物理地址 0x00000000。修改 GDT 中视频段描述符的基址使其高地址部分为 0xC0000000这样可以通过 GS 段寄存器访问 0xC00B8000 处的显存。将 GDT 的基址和栈指针 ESP 也映射到高地址空间。将页目录表的物理地址加载到 CR3 寄存器并开启分页机制设置 CR0 的 PG 位。重新加载 GDT使其位于高地址空间。解析并拷贝内核:调用子程序 kernel_init。解析内核文件 kernel.bin 的 ELF 格式头获取程序头表Program Header Table的位置和大小。遍历程序头表中的每一个段Segment根据其 p_vaddr虚拟地址、p_offset文件偏移和 p_filesz文件大小将内核的各个段从 0x70000 处拷贝到它们各自指定的虚拟地址处如 0xC0001500。跳转到内核入口点:设置一个新的栈指针 ESP 为 0xC009F000。执行 jmp KERNEL_ENTRY_POINT即跳转到 0xC0001500 处将控制权移交给内核。第三阶段内核 (Kernel) 执行内核在保护模式和分页机制下开始执行。执行 main 函数:内核的 main 函数被调用。其内容非常简单一个无限循环 while(1);。这表示操作系统已经成功启动并进入了稳定状态等待后续的中断或系统调用。jmp KERNEL_ENTRY_POINT 重点详解这条指令是整个引导加载程序Loader的终点也是操作系统内核Kernel的起点。它的执行标志着控制权从引导程序正式移交给了操作系统。指令的上下文在执行 jmp KERNEL_ENTRY_POINT 之前加载器已经完成了所有准备工作环境初始化已进入保护模式开启了分页机制GDT 和页表都已正确设置。内存布局内核文件 kernel.bin 已被读取到内存地址 0x70000 处。内核解析与拷贝通过 kernel_init 子程序内核的各个段如 .text 代码段、.data 数据段等已根据 ELF 文件头中的程序头表Program Header Table从 0x70000 处被精确地拷贝到了它们各自指定的虚拟地址上例如代码段可能被拷贝到 0xC0001000。栈指针重置栈指针 ESP 已被设置为 0xC009F000这是一个位于高地址空间的安全栈远离内核代码和数据区域。此时系统已准备好运行一个功能完整的 32 位保护模式下的程序。KERNEL_ENTRY_POINT 的定义在 boot.inc 头文件中KERNEL_ENTRY_POINT 被定义为KERNEL_ENTRY_POINT equ0xc0001500KERNEL_ENTRY_POINT equ 0xc0001500这意味着jmp KERNEL_ENTRY_POINT 等价于 jmp 0xC0001500。这个地址 0xC0001500 是一个虚拟地址。它是由内核开发者在链接脚本linker script中指定的通常是内核 main 函数的入口地址。当编译器编译 main.c 时会将 main 函数的机器码放在这个地址。执行过程详解当 CPU 执行到 jmp 0xC0001500 时会发生以下一系列事件第一步计算目标物理地址CPU 不直接访问虚拟地址它需要通过分页机制将其转换为物理地址。虚拟地址结构0xC0001500 的二进制表示为 1100 0000 0000 0000 0001 0101 0000 0000。最高 10 位 (1100000000) 是页目录索引 (Page Directory Index)即 768。中间 10 位 (0000000001) 是页表索引 (Page Table Index)即 1。最低 12 位 (010100000000) 是页内偏移 (Page Offset)。查找页目录项 (PDE)CPU 使用 CR3 寄存器中存储的页目录表基址在 setup_page 中设置为 0x100000。它找到页目录表的第 768 项因为虚拟地址最高 10 位是 768。根据 setup_page 中的设置第 768 项指向的是第一个页表其物理地址为 0x101000。查找页表项 (PTE)CPU 使用上一步得到的页表物理地址 0x101000。它找到该页表的第 1 项因为虚拟地址中间 10 位是 1。根据 setup_page 中的设置第 1 项指向的物理页帧地址是 0x1000因为第一个页表的第 0 项指向 0x00000000第 1 项指向 0x00001000。计算物理地址物理地址 页帧地址 页内偏移 0x00001000 0x500 0x00001500。因此虚拟地址 0xC0001500 被映射到物理地址 0x00001500。第二步跳转到目标地址更新 EIPCPU 将指令指针寄存器 EIP 设置为 0xC0001500。这意味着 CPU 下一条要执行的指令就是位于这个虚拟地址处的指令。刷新流水线由于这是一个无条件跳转CPU 会清空其指令预取队列和分支预测器中的内容确保下一条指令是从新的地址开始获取的。第三步执行内核代码main 函数入口0xC0001500 处的指令正是内核 main 函数的起始指令。intmain(void){while(1);return0;}编译后这会生成一条无限循环的汇编指令。无限循环CPU 开始执行 while(1); 对应的机器码进入一个永不退出的循环。这标志着操作系统已经成功启动并进入了稳定状态。关键点总结虚拟地址 vs 物理地址jmp 指令操作的是虚拟地址。真正的执行发生在由分页机制转换后的物理地址上。控制权完全移交一旦执行了 jmp加载器的代码就彻底停止运行CPU 完全按照内核的指令序列执行。环境准备至关重要jmp 能够成功执行的前提是加载器已经为内核搭建好了所有必要的运行环境包括保护模式、分页、正确的内存布局和栈。入口地址的约定0xC0001500 是一个约定俗成的地址它由内核的链接脚本决定确保了编译后的内核代码能够被正确地放置和执行。总结整个启动流程是一个环环相扣、层层递进的过程MBR 是“引路人”负责初始化基本环境加载并移交控制权给 加载器。加载器 是“工程师”负责完成从实模式到保护模式的复杂转换设置内存管理机制GDT 和分页并最终加载和定位 内核。内核 是“管理者”在获得控制权后开始执行其核心逻辑标志着操作系统的正式运行。这个流程展示了操作系统底层开发的核心思想通过精心设计的引导程序逐步建立起一个功能完备的运行环境为上层应用提供服务。