前言
由于涉及到马上要搞实习的事情,搞得我十分的浮躁,自己也是频繁失眠,想来还是自己太过懒了,没控制住自己,自己也在这一个多月没搞好,尤其是本来想花几天时间来写一个高性能服务器,也把游双大佬的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,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88的方法
;先算出低15MB的内存
;ax和cx是以kb为单位的内存数量,将其转化成以byte为单位
mov cx,0x400 ;cx和ax值都一样,cx用作乘数
mul cx
shl edx,16
and eax,0x0000ffff
or edx,eax
add edx,0x100000 ;ax只是15MB,故要加上1MB
mov esi,edx ;想把低15MB的内存容量存入esi作为备份
;再将16MB以上的内存转化成byte为单位
;寄存器bx和dx中是64kb为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx,0x10000 ;10机制是64kb
mul ecx ;32位乘法,默认的被乘数是eax,结果为64位
;高32位存入edx,低32位存入eax
add 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_hlt
and eax,0x0000ffff
;16为乘法,被乘数是ax,结果32位,高位16在dx,低32位在ax
mov cx,0x400
;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl 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:
;先把页目录占用的空间逐字节清0
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
;开始创建页目录项(PDE)
.create_pde: ; 创建Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
add 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, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址
;下面创建页表项(PTE)
mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1
.create_pte: ; 创建Page Table Entry
mov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址
add edx,4096
inc esi
loop .create_pte
;创建内核其它页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此时eax为第二个页表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 范围为第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret
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_init
mov esp, 0xc009f000
jmp KERNEL_ENTRY_POINT ; 用地址0x1500访问测试,结果ok
;----------将kernel.bin中的segment拷贝到编译的地址
kernel_init:
xor eax, eax
xor ebx, ebx ;ebx记录程序头表地址
xor ecx, ecx ;cx记录程序头表中的program header数量
xor edx, edx ;dx 记录program header尺寸,即e_phentsize
mov 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_ADDR
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
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
push ebp
mov ebp, esp
push ecx ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
mov edi, [ebp + 8] ; dst
mov esi, [ebp + 12] ; src
mov ecx, [ebp + 16] ; size
rep movsb ; 逐字节拷贝
;恢复环境
pop ecx
pop ebp
ret
代码汇总
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;构建gdt及其内部的描述符
GDT_BASE: 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
dd 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内存地址是0xb00
total_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 0
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
int 0x15
jc .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_area
jmp .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,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88的方法
;先算出低15MB的内存
;ax和cx是以kb为单位的内存数量,将其转化成以byte为单位
mov cx,0x400 ;cx和ax值都一样,cx用作乘数
mul cx
shl edx,16
and eax,0x0000ffff
or edx,eax
add edx,0x100000 ;ax只是15MB,故要加上1MB
mov esi,edx ;想把低15MB的内存容量存入esi作为备份
;再将16MB以上的内存转化成byte为单位
;寄存器bx和dx中是64kb为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx,0x10000 ;10机制是64kb
mul ecx ;32位乘法,默认的被乘数是eax,结果为64位
;高32位存入edx,低32位存入eax
add 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_hlt
and eax,0x0000ffff
;16为乘法,被乘数是ax,结果32位,高位16在dx,低32位在ax
mov cx,0x400
;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl 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,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 ;刷新流水线
[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 ;低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描述符中视频段描述符中的段基址+0xc0000000
mov ebx, [gdt_ptr + 2]
or dword [ebx + 0x18 + 4], 0xc0000000 ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
;段描述符的高4字节的最高位是段基址的31~24位
;将gdt的基址加上0xc0000000使其成为内核所在的高地址
add dword [gdt_ptr + 2], 0xc0000000
add esp, 0xc0000000 ; 将栈指针同样映射到内核地址
; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
;在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr] ; 重新加载
mov byte [gs:160], 'S' ;视频段段基址已经被更新,用字符v表示virtual addr
jmp SELECTOR_CODE:enter_kernel ;强制刷新流水线
enter_kernel:
call kernel_init
mov esp, 0xc009f000
jmp KERNEL_ENTRY_POINT ; 用地址0x1500访问测试,结果ok
;----------将kernel.bin中的segment拷贝到编译的地址
kernel_init:
xor eax, eax
xor ebx, ebx ;ebx记录程序头表地址
xor ecx, ecx ;cx记录程序头表中的program header数量
xor edx, edx ;dx 记录program header尺寸,即e_phentsize
mov 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_ADDR
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
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
push ebp
mov ebp, esp
push ecx ; rep指令用到了ecx,但ecx对于外层段的循环还有用,故先入栈备份
mov edi, [ebp + 8] ; dst
mov esi, [ebp + 12] ; src
mov ecx, [ebp + 16] ; size
rep movsb ; 逐字节拷贝
;恢复环境
pop ecx
pop ebp
ret
;------------- 创建页目录及页表 ---------------
setup_page:
;先把页目录占用的空间逐字节清0
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
;开始创建页目录项(PDE)
.create_pde: ; 创建Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
add 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, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址
;下面创建页表项(PTE)
mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1
.create_pte: ; 创建Page Table Entry
mov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址
add edx,4096
inc esi
loop .create_pte
;创建内核其它页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此时eax为第二个页表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 范围为第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret
;-------功能:读取硬盘n个扇区---------
rd_disk_m_32:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ; 备份eax
mov di,cx ; 备份扇区数到di
;读写硬盘:
;第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 ; 设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来
;第4步:检测硬盘状态
.not_ready: ;测试0x1f7端口(status寄存器)的的BSY位
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
cmp al,0x08
jnz .not_ready ;若未准备好,继续等。
;第5步:从0x1f0端口读数据
mov ax, di ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
;在此先用这种方法,在后面内容会用到insw和outsw等
mov dx, 256 ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
mul dx
mov cx, ax
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [ebx], ax
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
ret
实验结果&&注意事项
注意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的地址上,之后就跳转到这个地址上去执行内核了