elf文件
elf文件介绍
一个程序文件需要有程序头来说明程序的入口地址及其相关信息,如下所示
程序是由段(如代码段、数据段)和节组成的,因此在程序头中要有一个段头表(程序头表)和节头表来描述程序中各种段及节的信息,故
- 程序头表:也称段头表,其内元素用于描述程序中的各个段
- 节头表:其内元素用于描述程序中的各个节
由于程序头(段头)和节头的数量不固定,因此程序头表和节头表的大小也就不固定,因此需要一个数据结构来说明程序头表和节头表的大小和位置信息,这个数据结构就是elf
elf文件格式
ELF格式的作用体现在两方面,一是链接阶段:另一方面是运行阶段,故它们在文件中组织布局咱们也从这两方面展示,如图所示。
elf header结构
elf header的数据类型
elf header结构
- e_ident[16]:16字节,用来表示elf字符等信息,开头的4个字节是固定不变的,是elf文件的魔数,它们分别是0x7f,以及字符串ELF的asc码:0x45,0x4c,0x46
- e_type:2字节,指定elf目标文件的类型
- e_machine:2字节,描述elf目标文件要在那种硬件平台运行
- e_version:4字节,版本信息
- e_entry:占用4字节,用来指明操作系统运行该程序时,将控制权转交到的虚拟地址
- e_phoff(program header table offset):4字节,程序头表在文件内的字节偏移量
- e_shoff(section header table offset):4字节,节头表在文件内的偏移量
- e_flags:4字节,指明与处理器相关的标志
- e_ehsize:2字节,指明elf header字节大小
- e_phentsize;2字节,指明程序头表中每个条目(entry)的字节大小,也就是每个用来描述段信息的数据结构的字节大小
- e_phnum:2字节,程序头表中条目的数量
- e_shentsize:2字节,节头表中每个条目的字节大小
- e_shnum:2字节,节头表中条目的数量
- e_shstrndx:2字节,指明string name table在节头表中的索引index
程序头表中条目的数据结构
- p_type:4字节,程序中段的类型
- p_offset:4字节,本段在文件内的起始偏移地址
- p_vaddr:4字节,本段在内存中的起始虚拟地址
- p_paddr:4字节,暂且保留,未设定
- p_filez:4字节,本段在文件中的大小
- p_memsz:4字节,本段在内存中的大小
- p_flags:4字节,指明与本段相关的标志
- p_align:4字节,用来指明本段在文件和内存中的对齐方式。如果值为0或1,则表示不对齐。否则p_align应该是2的幂次数。
将内核载入内存
将loader中栈指针地址的宏转移到boot.inc中
LOADER_STACK_TOP equ LOADER_BASE_ADDR ;初始化栈指针地址
同时在boot.inc中定义内核相关的宏
KERNEL_BIN_BASE_ADDR equ 0x70000 ;内核文件加载到内存中的位置
KERNEL_START_SECTOR equ 0x9 ;内核文件在磁盘中的起始盘区
KERNEL_ENTRY_POINT equ 0xc0001500 ;定义内核可执行代码的入口地址
;----- 程序段的类型定义 ---------
PT_NULL equ 0
在分页机制开启之前将内核从磁盘加载到内存中
;------------- 加载内核 ----------------
;将内核从磁盘的9号扇区加载的内存的KERNEL_BIN_BASE_ADDR地址处
mov eax,KERNEL_START_SECTOR
mov ebx,KERNEL_BIN_BASE_ADDR
mov ecx,200
call rd_disk_m_32
rd_disk_m_32:
; eax=LBA扇区号
; 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 ; 设置7~4位为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
ret
遍历内存中内核的elf文件头,取出其内的每一个段头,再将其拷贝到内存中对应的位置
;------------- 进入内核函数 ----------------
enter_kernel:
call kernel_init
mov esp,0xc009f000
jmp KERNEL_ENTRY_POINT
kernel_init:
xor eax,eax
xor ebx,ebx ;记录程序头(段)地址
xor ecx,ecx ;记录程序头表(段头表)中程序头数量
xor edx,edx ;记录程序头(段)大小
mov dx,[KERNEL_BIN_BASE_ADDR+42] ;42字节处是e_phentsize,即程序头(段)大小
mov ebx,[KERNEL_BIN_BASE_ADDR+28] ;28字节处是e_phoff,即程序头表的偏移
add ebx,KERNEL_BIN_BASE_ADDR ;程序头表的偏移加上内核在内存中的起始地址,就是程序头表的虚拟起始地址
mov cx,[KERNEL_BIN_BASE_ADDR+44] ;44字节处是e_phnum,即程序头表(段头表)中程序头数量
;遍历段头表
.each_segment:
;检查段头表中的段头是会否是空段(PT_NULL),如果不是就将该段拷贝到对应区域,否则就继续遍历下一个段头
cmp byte [ebx+0],PT_NULL ;比较p_type是否等于PT_NULL,若相等说明程序头未使用
je .PTNULL ;若相等则跳转到.PTNULL
;为函数memcpy(dst,src,size)压入参数
push dword [ebx+16] ;实参size,程序头表偏移16字节的地方p_sizesz,本段在文件内的大小
mov eax,[ebx+4] ;程序头表偏移4字节的地方p_offset,本段在文件内的起始偏移
add eax,KERNEL_BIN_BASE_ADDR
push eax ;实参src
push dword [ebx+8] ;实参dst,p_vaddr,本段在内存中的起始虚拟地址
call mem_cpy
add esp,12 ;回收mem_cpy的栈帧空间
.PTNULL:
add ebx,edx ;指向下一个段头
loop .each_segment ;继续遍历段头表
ret
其中逐字节拷贝函数为
;逐字节拷贝函数,将esi指向的内存区域的size个字节拷贝到edi指向的区域
mem_cpy:
cld ;指明拷贝时esi与edi的增长方向是向上的
push ebp ;保存ebp
mov ebp,esp ;将esp指向ebp
push ecx ;由于rep指令会用到ecx的循环计数,
;而外层函数也用到了ecx的值,
;因此此处需要将外层函数的ecx的值进行备份
mov edi,[ebp+8] ;参数dst
mov esi,[ebp+12] ;参数src
mov ecx,[ebp+16] ;参数size
rep movsb ;rep(repeat)指令,重复执行movsb指令
;movsb指令,s表示string,b表示byte,
;即将esi指向的内存拷贝一个字节给edi指向的内存
;因此本条指令表示逐字节拷贝,拷贝的字节个数为ecx的值
pop ecx ;取出备份的值
pop ebp ;返回上层函数
ret
完整代码
boot.inc
;-----loader and kernel-----
LOADER_BASE_ADDR equ 0x900 ;loader在内存中位置
LOADER_START_SECTOR equ 0x2 ;loader在磁盘中的逻辑扇区地址,即LBA地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR ;初始化栈指针地址
PAGE_DIR_TABLE_POS equ 0x100000 ;页目录表基址
KERNEL_BIN_BASE_ADDR equ 0x70000 ;内核文件加载到内存中的位置
KERNEL_START_SECTOR equ 0x9 ;内核文件在磁盘中的起始盘区
KERNEL_ENTRY_POINT equ 0xc0001500 ;定义内核可执行代码的入口地址
;----- gdt描述符属性 ---------
DESC_G_4K equ 1_00000000000000000000000b ;设置段界限的单位为4KB
DESC_D_32 equ 1_0000000000000000000000b ;设置代码段/数据段的有效地址(段内偏移地址)及操作数大小为32位
DESC_L equ 0_000000000000000000000b ;64位代码段标记位,现在是32位操作系统,因此标记为0即可。
DESC_AVL equ 0_00000000000000000000b
;定义段界限位
;段界限的第2部分,即描述符的高32位中的第16~19位,最终的代码段段界限为0xFFFFF
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
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
;定义类型位
DESC_S_CODE equ 1_000000000000b ;代码段和数据段都是非系统段,故类型字段s设置为1
DESC_S_DATA equ DESC_S_CODE ;代码段和数据段都是非系统段,故类型字段s设置为1
DESC_S_sys equ 0_000000000000b ;系统段的类型字段设置为0
;定义子类型位
DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清0
DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0,数据段不可执行,向上扩展,可写,已访问位a清0
;拼接代码段的描述符
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
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
;----- 模块化的页目录表字段 ---------
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
;----- 程序段的类型定义 ---------
PT_NULL equ 0
loader.S
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
;------------- 构建gdt及其内部的描述符 -------------
GDT_BASE:
dd 0x00000000
dd 0x00000000
;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000
;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFF
CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4 ;段描述符的高4字节部分
DATA_STACK_DESC:
dd 0x0000FFFF
dd DESC_DATA_HIGH4
;定义显存段的描述符
;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型
;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,
;段粒度位4k,因此段界限的值为0x7fff/4k=7
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0 ;此处预留60个描述符的空位
;------------- 构建选择子 -------------
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 dd 0 ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节
;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200
;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),
;故total_mem_bytes在内存中的地址为0x900+0x200=0xc00
;该地址将来在内核中会被用到
;------------- 定义gdtr(指向GDT的寄存器) -------------
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
;该定义语句事实上是定义了一个数组ards_buf[244]
;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300
ards_nr dw 0 ;用于记录ards结构体数量
;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:
;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值
;EBX:ARDS后续值
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EDX:固定为签名标记,0x534d4150
;返回值
;CF:若cf为0,表示未出错,cf为1,表示调用出错
;EAX:字符串SMAP的ASCII码值,0x534d4150
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EBX:ARDS后续值,即下一个ARDS的位置。
;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。
;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构
loader_start:
xor ebx,ebx ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0
mov edx,0x534d4150
mov di,ards_buf ;di存储缓冲区地址,即指向缓冲区首地址
.e820_mem_get_loop:
mov eax,0x0000e820
mov ecx,20 ;一个ards结构体的大小
int 0x15 ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中
add di,cx ;使di增加20字节指向缓冲区中下一个的ARDS结构位置
inc word [ards_nr] ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量
cmp ebx,0 ;比较ebx中的值是否为0
jnz .e820_mem_get_loop ;若ebx不为0,则继续进行循环获取ARDS,
;若为0说明已经获取到最后一个ards,则退出循环
mov cx,[ards_nr] ;cx存储遍历到的ards结构体个数
mov ebx,ards_buf ;ebx指向缓冲区地址
xor edx,edx ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0
.find_max_mem_area:
mov eax,[ebx] ;eax用于遍历缓冲区中的每一个ards的BaseAddrLow
add eax,[ebx+8] ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLow
add ebx,20 ;遍历下一个ards
cmp edx,eax ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环
jge .next_ards
mov edx,eax ;否则就是更新edx
.next_ards:
loop .find_max_mem_area
mov [total_mem_bytes],edx ;将最终结果保存到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的PE位为1 -------------
mov eax,cr0
or eax,0x00000001
mov cr0,eax
jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
.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 ;初始化栈指针,将栈指针地址放入bsp寄存器
mov ax,SELECTOR_VIDEO ;初始化显存段寄存器,显存段的选择子放入gs寄存器
mov gs,ax
;------------- 加载内核 ----------------
;将内核从磁盘的9号扇区加载的内存的KERNEL_BIN_BASE_ADDR地址处
mov eax,KERNEL_START_SECTOR
mov ebx,KERNEL_BIN_BASE_ADDR
mov ecx,200
call rd_disk_m_32
;------------- 开启分页机制 ----------------
call setup_page ;创建页目录表和页表,并初始化页内存位图
mov ebx,[gdt_ptr+2] ;gdt_ptr+2表示GDT_BASE,也就是GDT的起始地址
or dword [ebx+0x18+4],0xc0000000 ;ebx中保存着GDT_BASE,0x18=24,故ebx+0x18表示取出显存段的起始地址
;+4表示取出段描述符的高32位,之后和0xc0000000进行或操作
add dword [gdt_ptr+2],0xc0000000 ;同理将GDT_BASE的起始地址也增加3G
add esp,0xc0000000 ;同理将esp栈指针的起始地址也增加3G
mov eax,PAGE_DIR_TABLE_POS
mov cr3,eax
mov eax,cr0 ;打开cr0的PG位
or eax,0x80000000
mov cr0,eax
lgdt [gdt_ptr] ;开启分页后,用gdt的新地址重新加载
;------------- 进入内核函数 ----------------
enter_kernel:
call kernel_init
mov esp,0xc009f000
jmp KERNEL_ENTRY_POINT
kernel_init:
xor eax,eax
xor ebx,ebx ;记录程序头(段)地址
xor ecx,ecx ;记录程序头表(段头表)中程序头数量
xor edx,edx ;记录程序头(段)大小
mov dx,[KERNEL_BIN_BASE_ADDR+42] ;42字节处是e_phentsize,即程序头(段)大小
mov ebx,[KERNEL_BIN_BASE_ADDR+28] ;28字节处是e_phoff,即程序头表的偏移
add ebx,KERNEL_BIN_BASE_ADDR ;程序头表的偏移加上内核在内存中的起始地址,就是程序头表的虚拟起始地址
mov cx,[KERNEL_BIN_BASE_ADDR+44] ;44字节处是e_phnum,即程序头表(段头表)中程序头数量
;遍历段头表
.each_segment:
;检查段头表中的段头是会否是空段(PT_NULL),如果不是就将该段拷贝到对应区域,否则就继续遍历下一个段头
cmp byte [ebx+0],PT_NULL ;比较p_type是否等于PT_NULL,若相等说明程序头未使用
je .PTNULL ;若相等则跳转到.PTNULL
;为函数memcpy(dst,src,size)压入参数
push dword [ebx+16] ;实参size,程序头表偏移16字节的地方p_sizesz,本段在文件内的大小
mov eax,[ebx+4] ;程序头表偏移4字节的地方p_offset,本段在文件内的起始偏移
add eax,KERNEL_BIN_BASE_ADDR
push eax ;实参src
push dword [ebx+8] ;实参dst,p_vaddr,本段在内存中的起始虚拟地址
call mem_cpy
add esp,12 ;回收mem_cpy的栈帧空间
.PTNULL:
add ebx,edx ;指向下一个段头
loop .each_segment ;继续遍历段头表
ret
;逐字节拷贝函数,将esi指向的内存区域的size个字节拷贝到edi指向的区域
mem_cpy:
cld ;指明拷贝时esi与edi的增长方向是向上的
push ebp ;保存ebp
mov ebp,esp ;将esp指向ebp
push ecx ;由于rep指令会用到ecx的循环计数,
;而外层函数也用到了ecx的值,
;因此此处需要将外层函数的ecx的值进行备份
mov edi,[ebp+8] ;参数dst
mov esi,[ebp+12] ;参数src
mov ecx,[ebp+16] ;参数size
rep movsb ;rep(repeat)指令,重复执行movsb指令
;movsb指令,s表示string,b表示byte,
;即将esi指向的内存拷贝一个字节给edi指向的内存
;因此本条指令表示逐字节拷贝,拷贝的字节个数为ecx的值
pop ecx ;取出备份的值
pop ebp ;返回上层函数
ret
;------------- 创建页目录表和页表 -------------
;初始化页目录表和页表
;逐字节清空页目录表
setup_page:
mov ecx,4096 ;页目录表的大小为4KB,ecx是loop指令的循环计数器
;由于初始化页表是逐字节置0的,因此ecx的值为4096
mov esi,0 ;页目录表的偏移量
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS+esi],0 ;逐字节清空页目录表
;其中PAGE_DIR_TABLE_POS为页目录表初始地址的宏
inc esi ;递增偏移量,清空下一个字节
loop .clear_page_dir
;初始化创建页目录表
.create_pde:
mov eax,PAGE_DIR_TABLE_POS ;eax保存页目录表的起始地址
add eax,0x1000 ;0x1000为1k,故该代码的计算结果是将eax指向第一张页表的起始地址
mov ebx,eax ;ebx保存第一张页表的起始地址,后续会用到
or eax,PG_US_U|PG_RW_W|PG_P ;eax已经有了第一张页表的起始地址
;此处再加上属性,即可表示为页目录表的一个表项,
;该表项代表的是第一张页表的物理地址及其相关属性
mov [PAGE_DIR_TABLE_POS+0x0],eax ;页目录表的第一个表项指向第一张页表
mov [PAGE_DIR_TABLE_POS+0xc00],eax ;0xc0000000即为3GB,由于我们划分的虚拟地址空间3GB以上为os地址空间
;因此该语句是将3GB的虚拟空间映射到内核空间
;而0xc00/4=768,也就是说页目录表的768号表项映射为物理内核空间
sub eax,0x1000
mov [PAGE_DIR_TABLE_POS+4092],eax ;最后一个页表项指向自己,为将来动态操作页表做准备
;创建第一张页表的页表项,由于os的物理内存不会超过1M,故页表项个数的最大值为1M/4k=256
mov ecx,256 ;循环计数器
mov esi,0 ;偏移量
mov edx,PG_US_S|PG_RW_W|PG_P ;此时的edx表示拥有属性PG_US_S|PG_RW_W|PG_P
;且物理地址为0的物理页的页表项
.create_pte:
mov [ebx+esi*4],edx ;此前ebx已经保存了第一张页表的起始地址
add edx,4096 ;edx指向下一个物理页(一个物理页4KB)
inc esi ;esi指向页表的下一个偏移
loop .create_pte
; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推
mov eax,PAGE_DIR_TABLE_POS
add eax,0x2000 ;此时的eax表示第二张页表的起始地址
or eax,PG_US_U|PG_RW_W|PG_P ;为eax表项添加属性
mov ebx,PAGE_DIR_TABLE_POS
mov ecx,254 ;要设置254个页表项
mov esi,769 ;从第769个页表项开始设置
.create_kernel_pde:
mov [ebx+esi*4],eax ; 设置页目录表项
inc esi ; 增加要设置的页目录表项的偏移
add eax,0x1000 ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项
loop .create_kernel_pde ; 循环设定254个页目录表项
ret
;-------------------------------------------------------------------------------
rd_disk_m_32:
; eax=LBA扇区号
; 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 ; 设置7~4位为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
ret
结果
dd if=./build/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc