前言

由于涉及到马上要搞实习的事情,搞得我十分的浮躁,自己也是频繁失眠,想来还是自己太过懒了,没控制住自己,自己也在这一个多月没搞好,尤其是本来想花几天时间来写一个高性能服务器,也把游双大佬的linux 高性能服务器编程大概看完了,然后自己也跟着视频写了写,但总是感觉自己没有真的懂,然后自己在github上下来的项目也总是感觉不好下手,然后我自己又想去搞csapp的实验了,结果前几个实验还好,但是在做那个shell lab的时候总感觉自己没有很好的搞懂,遂又放弃,然后自己又迷上了原神,又是荒废了十几天,这一个多月来真的自己心态一直有问题,就是自己急于求快,对于我这样的没天赋的选手来讲,是最不能急于求快的,我很清楚了自己的天赋,想靠几天时间就开发一个比较完整的高性能服务器也不现实,同样几天时间做完csapp实验也不太现实
痛定思痛,仔细观察了自己的缺点,容易半途而废,容易拖时间,容易被分心,我是不适合像操作系统一样不停的进行上下文切换的,反之结果就会导致我实验没完成,项目没完成,操作系统也没写完,所以我以后比较大的事情我直接专心一件事情更好。
同时,我也只能等秋招了,实在是没啥信心来暑期实习啊。但是也决定不能再拖了,再拖就彻底凉了,我很清楚还有大概两个月来准备,基于对操作系统的热爱,我决定一定要把操作系统写完去,之后便着手准备八股文。

实验

5.1获取物理内存容量

关于获取物理内存容量就是根据中断子功能来判断就可以了,这书中已经说的很详细了,也就没啥可说的。

代码

;----int 15h ax=e801h获取内存大小,最大支持4G
;返回后,ax cx值一样,以1kb为单位 bx,dx 值一样,以63kb为单位
;在ax和cx寄存器中为低16MB,在bx,dx寄存器中为16MB到4GB.e820_failed_so_try_e801:mov ax,0xe801int 0x15jc .e801_failed_so_try88    ;若当前e801方法失败,就尝试0x88的方法;先算出低15MB的内存
;ax和cx是以kb为单位的内存数量,将其转化成以byte为单位mov cx,0x400            ;cx和ax值都一样,cx用作乘数mul cxshl edx,16and eax,0x0000ffffor edx,eaxadd edx,0x100000        ;ax只是15MB,故要加上1MBmov esi,edx             ;想把低15MB的内存容量存入esi作为备份;再将16MB以上的内存转化成byte为单位
;寄存器bx和dx中是64kb为单位的内存数量xor eax,eaxmov ax,bxmov ecx,0x10000         ;10机制是64kbmul ecx                 ;32位乘法,默认的被乘数是eax,结果为64位;高32位存入edx,低32位存入eaxadd esi,eax
;由于此方法只能测出4GB以内的内存,故32位eax足够了
;edx肯定为0,只加eax便可mov edx,esi                 ;edx为总内存大小jmp .mem_get_ok;---int 15h ah=0x88获取内存大小,只能获取64MB之内
.e801_failed_so_try88:;int 15后,ax存入的是以kb为单位的内存容量mov ah,0x88 int 0x15 jc .error_hltand eax,0x0000ffff;16为乘法,被乘数是ax,结果32位,高位16在dx,低32位在axmov cx,0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位mul cxshl edx,16              ;把dx移动到高16位or edx,eax              ;把结果低16位组合到edx,为32位的结果add ebx,0x100000        ;0x88子功能只会返回1MB内存,故实际内存要加上1MB.error_hlt:hlt    ;暂停的意思.mem_get_ok:mov [total_mem_bytes],edx   ;将内存转换为byte后存入total_mem_bytes处

5.2 启用内存分页机制,畅游虚拟空间

对于这一部分,毫无疑问是这一章最精彩的部分,先说一下为啥要分页,主要是分段是有问题的,首先是内存碎片,同时由于段是连续的,也不好换入换出,于是就提出分页。通过页表的映射,这时的段基址:偏移地址也就不再是物理地址,而称作线性地址,或者虚拟地址,那页的大小也需要考量,不能太小,不然页表的内存容量就太大了,这样反而得不偿失,现在统一的页的大小是4kb,这样的话,对于32位机器来说,也就是一共4G的空间,4G/4kb=2^20=1MB,这其实还是挺大的,要知道我们这个操作系统的内核也就准备了1MB的物理内存,所以有2级页表,当然肯定主要不是由于页表的内存容量的问题,我从哈工大操作系统中可以知道,主要是对于页表项的索引,操作系统需要查找,但是页表项过多的话,就查找很费时间,而使用二级页表就能有效的避免这个问题,毕竟2级页表后,无论是页目录项还是页表项都只有1024个,这当然很轻松的就能查找。现代操作系统也一般用2级页表,所以书中也用2级页表。一级页表也就是页目录表存储的是页目录项,页目录项中有页表的物理地址(不是虚拟地址),页表中存储的就是页表项,页表项中有虚拟地址对应的物理地址。总而言之总的流程是这样的,首先,通过全局描述符(GDT)拿到线性地址,然后由于地址是32位的,操作系统把高10位作为页目录表的索引,而中10位作为页表中页表下项的索引,低12位就作为偏移地址+通过页表映射得到的地址最终得到虚拟地址所对应的物理地址。
当然,在实际中第1023个页目录项是没用的(从0开始数),因为存储的是页目录表自己的物理地址,无论是页目录表还是页表肯定都存储在内存中,所以有自己的物理地址,而为啥这样,这主要是为了方便定位到自己想要的页目录项和页表。

代码

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0mov ecx, 4096mov esi, 0
.clear_page_dir:mov byte [PAGE_DIR_TABLE_POS + esi], 0inc esiloop .clear_page_dir;开始创建页目录项(PDE)
.create_pde:                     ; 创建Page Directory Entrymov eax, PAGE_DIR_TABLE_POSadd eax, 0x1000                  ; 此时eax为第一个页表的位置及属性mov ebx, eax                     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备or eax, PG_US_U | PG_RW_W | PG_P         ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.sub eax, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax      ; 使最后一个目录项指向页目录表自己的地址;下面创建页表项(PTE)mov ecx, 256                  ; 1M低端内存 / 每页大小4k = 256mov esi, 0mov edx, PG_US_U | PG_RW_W | PG_P         ; 属性为7,US=1,RW=1,P=1
.create_pte:                     ; 创建Page Table Entrymov [ebx+esi*4],edx                ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 add edx,4096inc esiloop .create_pte;创建内核其它页表的PDEmov eax, PAGE_DIR_TABLE_POSadd eax, 0x2000            ; 此时eax为第二个页表的位置or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1mov ebx, PAGE_DIR_TABLE_POSmov ecx, 254               ; 范围为第769~1022的所有目录项数量mov esi, 769
.create_kernel_pde:mov [ebx+esi*4], eaxinc esiadd eax, 0x1000loop .create_kernel_pderet

5.3 加载内核

关于加载内核我们所知道的是,对于汇编来说我们可以指定文件的入口地址,利用jmp指令来跳转就够了,但是对于.c文件来说,可无法指定其地址,并且其地址弄成静态也不好,而对于.c文件来说一共要进行预处理->编译->汇编->链接这4个步骤,在没链接前生成的是目标文件,也就是可重定位文件,可重定位也就是其加载到内存的地址还未确定,需要通过链接来确定,链接后才可以得到其入口地址,因为对于文件来说起始分为两部分,一部分是文件头,这个部分存储着入口地址等信息,而另一部分就是程序部分,也就是由cpu执行的那些部分。而我们要加载内核首先要做的就是确定kernel.bin的入口地址。

代码

gcc -c -o main.o main.c
ld main.o -Ttext 0xc0001500 -e main -o kernel.bin
enter_kernel:    call kernel_initmov esp, 0xc009f000jmp KERNEL_ENTRY_POINT                 ; 用地址0x1500访问测试,结果ok;----------将kernel.bin中的segment拷贝到编译的地址
kernel_init:xor eax, eaxxor ebx, ebx        ;ebx记录程序头表地址xor ecx, ecx        ;cx记录程序头表中的program header数量xor edx, edx     ;dx 记录program header尺寸,即e_phentsizemov dx, [KERNEL_BIN_BASE_ADDR + 42]   ; 偏移文件42字节处的属性是e_phentsize,表示program header大小mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值add ebx, KERNEL_BIN_BASE_ADDRmov 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的第三个参数:sizemov eax, [ebx + 4]            ; 距程序头偏移量为4字节的位置是p_offsetadd 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_segmentret;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:              cldpush ebpmov ebp, esppush ecx          ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份mov edi, [ebp + 8]       ; dstmov esi, [ebp + 12]       ; srcmov ecx, [ebp + 16]       ; sizerep movsb         ; 逐字节拷贝;恢复环境pop ecx      pop ebpret

代码汇总

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR;构建gdt及其内部的描述符
GDT_BASE:   dd 0x00000000dd 0x00000000
CODE_DESC:  dd 0x0000ffffdd DESC_CODE_HIGH4
DATA_STACK_DESC:    dd 0x0000ffffdd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007dd DESC_VIDEO_HIGH4 GDT_SIZE  equ  $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0
SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0;total_mem_bytes 用于保存内存容量
;当前偏移loader.bin文件头0x200字节,也就是512字节
;loader.bin的加载地址是0x900
;故total_mem_bytes内存地址是0xb00total_mem_bytes dd 0;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr  dw GDT_LIMIT dd GDT_BASE;人工对齐:total_mem_bytes4+gdt_ptr6+ards_buf244+ards_nr2,共256字节ards_buf times 244 db 0
ards_nr dw 0loader_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,20int 0x15jc .e820_failed_so_try_e801     ;若cf位有1则有错误发生,尝试0xe801子功能add di,cx                       ;使di增加20字节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,[ebx+8]         ;length_low
add ebx,20              ;指向缓冲区的下一个ards结构
cmp edx,eax             ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge  .next_ards
mov edx,eax.next_ards:loop .find_max_mem_areajmp .mem_get_ok;----int 15h ax=e801h获取内存大小,最大支持4G
;返回后,ax cx值一样,以1kb为单位 bx,dx 值一样,以63kb为单位
;在ax和cx寄存器中为低16MB,在bx,dx寄存器中为16MB到4GB.e820_failed_so_try_e801:mov ax,0xe801int 0x15jc .e801_failed_so_try88    ;若当前e801方法失败,就尝试0x88的方法;先算出低15MB的内存
;ax和cx是以kb为单位的内存数量,将其转化成以byte为单位mov cx,0x400            ;cx和ax值都一样,cx用作乘数mul cxshl edx,16and eax,0x0000ffffor edx,eaxadd edx,0x100000        ;ax只是15MB,故要加上1MBmov esi,edx             ;想把低15MB的内存容量存入esi作为备份;再将16MB以上的内存转化成byte为单位
;寄存器bx和dx中是64kb为单位的内存数量xor eax,eaxmov ax,bxmov ecx,0x10000         ;10机制是64kbmul ecx                 ;32位乘法,默认的被乘数是eax,结果为64位;高32位存入edx,低32位存入eaxadd esi,eax
;由于此方法只能测出4GB以内的内存,故32位eax足够了
;edx肯定为0,只加eax便可mov edx,esi                 ;edx为总内存大小jmp .mem_get_ok;---int 15h ah=0x88获取内存大小,只能获取64MB之内
.e801_failed_so_try88:;int 15后,ax存入的是以kb为单位的内存容量mov ah,0x88 int 0x15 jc .error_hltand eax,0x0000ffff;16为乘法,被乘数是ax,结果32位,高位16在dx,低32位在axmov cx,0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位mul cxshl edx,16              ;把dx移动到高16位or edx,eax              ;把结果低16位组合到edx,为32位的结果add ebx,0x100000        ;0x88子功能只会返回1MB内存,故实际内存要加上1MB.error_hlt:hlt.mem_get_ok:mov [total_mem_bytes],edx   ;将内存转换为byte后存入total_mem_bytes处;-------准备进入保护模式----------;1 打开A20;2 加载gdt;3 将cr0的pe 为置1;---------------------------打开A20----------in al,0x92or al,0000_0010bout 0x92,al;------------------------加载GDT------------------lgdt [gdt_ptr];;----------------------cr0第0位置1-------------------mov eax,cr0 or eax,0x00000001mov cr0,eaxjmp dword SELECTOR_CODE:p_mode_start ;刷新流水线[bits 32]p_mode_start:mov ax,SELECTOR_DATAmov ds,axmov es,axmov ss,axmov esp,LOADER_STACK_TOPmov ax,SELECTOR_VIDEO        ;低32位放入显存中mov gs,ax
;--------加载kernel---------mov eax, KERNEL_START_SECTOR        ; kernel.bin所在的扇区号mov ebx, KERNEL_BIN_BASE_ADDR       ; 从磁盘读出后,写入到ebx指定的地址mov ecx, 200                ; 读入的扇区数; 创建页目录及页表并初始化页内存位图call setup_page;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载sgdt [gdt_ptr]        ; 存储到原来gdt所有的位置;将gdt描述符中视频段描述符中的段基址+0xc0000000mov ebx, [gdt_ptr + 2]  or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。;段描述符的高4字节的最高位是段基址的31~24位;将gdt的基址加上0xc0000000使其成为内核所在的高地址add dword [gdt_ptr + 2], 0xc0000000add esp, 0xc0000000        ; 将栈指针同样映射到内核地址; 把页目录地址赋给cr3mov eax, PAGE_DIR_TABLE_POSmov cr3, eax; 打开cr0的pg位(第31位)mov eax, cr0or eax, 0x80000000mov cr0, eax;在开启分页后,用gdt新的地址重新加载lgdt [gdt_ptr]             ; 重新加载mov byte [gs:160], 'S'     ;视频段段基址已经被更新,用字符v表示virtual addrjmp SELECTOR_CODE:enter_kernel ;强制刷新流水线enter_kernel:    call kernel_initmov esp, 0xc009f000jmp KERNEL_ENTRY_POINT                 ; 用地址0x1500访问测试,结果ok;----------将kernel.bin中的segment拷贝到编译的地址
kernel_init:xor eax, eaxxor ebx, ebx        ;ebx记录程序头表地址xor ecx, ecx        ;cx记录程序头表中的program header数量xor edx, edx     ;dx 记录program header尺寸,即e_phentsizemov dx, [KERNEL_BIN_BASE_ADDR + 42]   ; 偏移文件42字节处的属性是e_phentsize,表示program header大小mov ebx, [KERNEL_BIN_BASE_ADDR + 28]   ; 偏移文件开始部分28字节的地方是e_phoff,表示第1 个program header在文件中的偏移量; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值add ebx, KERNEL_BIN_BASE_ADDRmov 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的第三个参数:sizemov eax, [ebx + 4]            ; 距程序头偏移量为4字节的位置是p_offsetadd 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_segmentret;----------  逐字节拷贝 mem_cpy(dst,src,size) ------------
;输入:栈中三个参数(dst,src,size)
;输出:无
;---------------------------------------------------------
mem_cpy:              cldpush ebpmov ebp, esppush ecx          ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份mov edi, [ebp + 8]       ; dstmov esi, [ebp + 12]       ; srcmov ecx, [ebp + 16]       ; sizerep movsb         ; 逐字节拷贝;恢复环境pop ecx      pop ebpret;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0mov ecx, 4096mov esi, 0
.clear_page_dir:mov byte [PAGE_DIR_TABLE_POS + esi], 0inc esiloop .clear_page_dir;开始创建页目录项(PDE)
.create_pde:                     ; 创建Page Directory Entrymov eax, PAGE_DIR_TABLE_POSadd eax, 0x1000                  ; 此时eax为第一个页表的位置及属性mov ebx, eax                     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备or eax, PG_US_U | PG_RW_W | PG_P         ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.sub eax, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax      ; 使最后一个目录项指向页目录表自己的地址;下面创建页表项(PTE)mov ecx, 256                  ; 1M低端内存 / 每页大小4k = 256mov esi, 0mov edx, PG_US_U | PG_RW_W | PG_P         ; 属性为7,US=1,RW=1,P=1
.create_pte:                     ; 创建Page Table Entrymov [ebx+esi*4],edx                ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 add edx,4096inc esiloop .create_pte;创建内核其它页表的PDEmov eax, PAGE_DIR_TABLE_POSadd eax, 0x2000            ; 此时eax为第二个页表的位置or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1mov ebx, PAGE_DIR_TABLE_POSmov ecx, 254               ; 范围为第769~1022的所有目录项数量mov esi, 769
.create_kernel_pde:mov [ebx+esi*4], eaxinc esiadd eax, 0x1000loop .create_kernel_pderet;-------功能:读取硬盘n个扇区---------
rd_disk_m_32:
;-------------------------------------------------------------------------------; eax=LBA扇区号; ebx=将数据写入的内存地址; ecx=读入的扇区数mov esi,eax     ; 备份eaxmov di,cx        ; 备份扇区数到di
;读写硬盘:
;第1步:设置要读取的扇区数mov dx,0x1f2mov al,clout dx,al            ;读取的扇区数mov eax,esi      ;恢复ax;第2步:将LBA地址存入0x1f3 ~ 0x1f6;LBA地址7~0位写入端口0x1f3mov dx,0x1f3                       out dx,al                          ;LBA地址15~8位写入端口0x1f4mov cl,8shr eax,clmov dx,0x1f4out dx,al;LBA地址23~16位写入端口0x1f5shr eax,clmov dx,0x1f5out dx,alshr eax,cland al,0x0f     ;lba第24~27位or al,0xe0       ; 设置7~4位为1110,表示lba模式mov dx,0x1f6out dx,al;第3步:向0x1f7端口写入读命令,0x20 mov dx,0x1f7mov al,0x20                        out dx,al;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来;第4步:检测硬盘状态.not_ready:          ;测试0x1f7端口(status寄存器)的的BSY位;同一端口,写时表示写入命令字,读时表示读入硬盘状态nopin al,dxand al,0x88     ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙cmp al,0x08jnz .not_ready       ;若未准备好,继续等。;第5步:从0x1f0端口读数据mov ax, di    ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,;在此先用这种方法,在后面内容会用到insw和outsw等mov dx, 256    ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256mul dxmov cx, ax     mov dx, 0x1f0.go_on_read:in ax,dx        mov [ebx], axadd 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_readret

实验结果&&注意事项


注意gcc版本为4.4版本比较好,并且链接需要时32位的

gcc -m32 -c -o main.o main.c
ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin

总结

至此,这前5章也正式的结束了,也终于是踏入了内核,好好的回顾这前五章的工作。
首先计算机一开机便会把cs:ip的地址自动指向0xf000:0xfff0,这样地址就是0xffff0,还剩下16字节根本就不够bios进行一些配置,于是就会跳转到另一个地址来执行,然后在对扇区进行检查的时候就会碰到mbr.bin,于是就会加载mbr,加载到地址为0x7c00地址上,之后,mbr执行,并且读硬盘,此时就会把loader.bin加载到地址为0x900的地址上,之后在loader.bin中执行读物理内存,设置GDT,开启分页,读硬盘加载kernel.bin到0x700000地址上,之后会把内核映象拷贝到地址为0x1500的地址上,之后就跳转到这个地址上去执行内核了

参考

感谢这位大佬博客操作系统还原第5章

操作系统真象还原第5章:保护模式进阶,向内核进阶相关推荐

  1. 《操作系统真象还原》第二章

    <操作系统真象还原>第二章 编写MBR主引导记录 载入内存 过程: (1)程序被加载器(软件或硬件)加载到内存某个区域. (2)CPU的cs:ip寄存器被指向这个程序的起始地址. 从按下主 ...

  2. 《操作系统真象还原》第九章

    <操作系统真象还原>第九章 本篇对应书籍第九章的内容 本篇内容介绍了线程是什么,多线程的原理,多线程用到的核心数据结构,以及通过代码实现了内核多线程的调度 线程是什么? 执行流 过去,计算 ...

  3. 《操作系统真象还原》1-3章 学习记录

    文章目录 前言 一.开始实验前的一些基本问题解答? section的含义? vstart的含义? $ 和 $$区别? 实模式的特点? CPU如何和硬盘进行交互? CPU和IO设备交互方式? 程序载入内 ...

  4. 操作系统真象还原——第5章 从保护模式到内核

    目录 前言 5.1 获取物理内存容量 5.1.1 学习Linux获取内存的方法 5.1.2 实战内存容量检测 5.2 内存分页 为什么需要分页? 一级页表 二级页表 如何设计一个页表 分页机制的代码实 ...

  5. 《操作系统真象还原》第九章 ---- 终进入线程动斧开刀 豁然开朗拨云见日 还需解决同步机制才能长舒气

    文章目录 专栏博客链接 相关查阅博客链接 本书中错误勘误 进程 线程的自我小理解 线程 进程的状态 内核级线程 & 用户级线程 初步实现内核级线程 浪费两三个小时调试的辛酸史 编写thread ...

  6. 操作系统真象还原第2章:编写MBR主引导记录

    前言 这章的内容挺少的,也很简单,如果环境没配置错的话是没啥问题的.但是这章也很精彩,把引导的过程给说了出来,我也是看了几遍把这个过程给大致看懂了. 首先计算机一开机这个时cpu会自动把cs:ip指针 ...

  7. 操作系统真象还原第3章:完善MBR

    前言 这次我出现了一些BUG,导致我忙活了一阵子,还好的是解决了 老规矩还是把整个流程过一遍,当MBR忙活完后,就需要把自己手中的棒交给loader了,MBR的作用就是从硬盘中的内核加载器移动到内存中 ...

  8. 《操作系统真象还原》第二章 ---- 编写MBR主引导记录 初尝编写的快乐 雏形已显!

    文章目录 专栏博客链接 前引 相关术语 理清操作系统启动程序运行流程(部分) 编写MBR引导内容 编译并检验mbr.bin Linux dd 磁盘操作指令与参数 模拟操作试一试 结束语 专栏博客链接 ...

  9. 操作系统真象还原第4章:保护模式入门

    前言 妈的还是没控制住自己,玩了几天,我自己就有一个很臭的毛病,如果玩的话就会一直玩下去,既然如此我直接不玩了,妈的我发誓绝逼不玩了 还好没出现什么BUG,也算是完成了. 关于这一章,我觉得比较重要的 ...

最新文章

  1. CCNA10月27日战报
  2. 关于main函数的(int argc,char *argv[])
  3. 入门系列之使用Sysdig监视您的Ubuntu 16.04系统 1
  4. python的imread、newaxis
  5. python 多进程multiprocessing进程池pool tensorflow-yolov3 报错 MemoryError
  6. 马鞍山职业计算机考试,2020年职业适应性(技能)测试纲要
  7. CCIE理论第三篇-LISP技术
  8. FormData对象提交表单及上传图片/文件
  9. c语言中islower是什么函数,C语言islower函数介绍、示例和实现
  10. 使用css让文字两端对齐
  11. 普洛斯库列科夫 线性代数习题集_转载)科大学长对数学系学弟学妹的忠告
  12. 详情页html源代码,仿新浪首页、主题、详情页,纯html静态页面
  13. wordpress 中 erphpdown 短代码
  14. 终于找到一个功能全面的番茄钟时间管理工具:myPomodoro for Mac
  15. 匿名访问ftp服务器
  16. 好看的滚动条样式,css实现好看的滚动条样式
  17. 构建器builder模式 + lombok @Builder的介绍及使用
  18. 微信小程序——获取用户的运动步数
  19. Jenkins凭据导出
  20. js 生成唯一uuid

热门文章

  1. 同程旅游火车票部门面经
  2. JS手机浏览器判断(转)
  3. 位图php,ps中什么是位图
  4. 基于AIGC的3D场景创作引擎概述
  5. 如何准备机器学习数据集_数据准备技术及其在机器学习中的重要性
  6. 当婚纱摄影邂逅超级表格|流程监控
  7. 2016年8月31号
  8. 28人买可乐喝,3个可乐瓶盖可以换一瓶可乐
  9. Android向通讯录添加联系人的一般方法
  10. n阶差分方程重根计算公式的一般证明