《操作系统真象还原》第五章——加载内核

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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值