内核加载器加载内核

  • 操作系统如何加载二进制程序?
    操作系统本身是一个程序,操作系统加载程序可以简单看成: 用一个程序去调用另一个程序。最简单的做法就是 jmpcall 指令,比如我们的BIOS就是这样调用mbr,mbr再调用loader,但之前调用方法是很不灵活的,比如mbr的地址是0x7c00,loader的地址是0x900。这2个加载地址是固定的,调用方需要和被调用方提前约定加载地址。操作系统要加载那么多的程序,是不可能一一约定的。

    有没有一种方法让加载地址不那么固定?
    由于每个程序是单独存在的,所以程序的入口地址信息需要与程序绑定,最简单的方法就是在程序文件中专门腾出个空间来写入程序的入口地址。主调程序在该程序文件的相应空间中将该程序的入口信息读出来,将其加载到相应的入口地址,再跳转过去执行。(我们要实现的"内核加载器加载内核",大概思想也是如此),当然不仅仅是写程序的入口地址,也需要写很多东西,最后这些东西就构成了 程序头。于是程序就有 "程序头" + "程序体"。

内核加载器加载内核_第1张图片
包含程序头的程序文件.png
elf格式的二进制文件

ELF指的是,可执行链接格式,它是应用程序的二进制接口。
ELF目标格式文件类型:

  • 待重定位文件
    待重定位文件属于源文件编译后但未完成链接的半成品,它被用于与其他目标文件合并链接,一构建出二进制可执行文件或动态链接库。
    为什么称其为待重定位呢?
    因为在该目标文件中,如果引用了其他外部文件(其他目标文件或库文件)中定义的符号(全局变量或者全局函数等),在编译阶段只能先标识出一个符号名,该符号的地址还不能确定,因为不知道该符号是在哪个外部文件中,该外部文件需要被重定位后才能确定文件内的符号地址,这个重定位的工作是在链接过程中完成的。
  • 共享目标文件
    在可执行文件被加载的过程中被动态链接,成为程序代码的一部分。
  • 可执行文件
    进过编译链接后的,可以直接运行的程序文件。

程序中最重要的部分就是段和节,它们是真正的程序体。段比如代码段和数据段等,同样也有很多节,段是由节组成的,多个节经过链接之后就被合并成一个段了。
段和节的起始地址和大小信息是在用header中描述的。分别是程序头表(program header table)和节头表(section header table)。里面描述的是程序头和节头信息。在这2个表中,每个元素称之为条目

ELF格式的作用体现在2个方面: 一是链接阶段,二是运行阶段

内核加载器加载内核_第2张图片
ELF格式.png
Elf32_Ehdr

下面就是elf32格式的header。

typedef uint16_t Elf32_Half;     /* 2字节 */
typedef uint32_t Elf32_Word;     /* 4字节 */
typedef uint32_t Elf32_Off;      /* 4字节 */
typedef uint32_t Elf32_Addr;     /* 4字节 */

struct Elf32_Ehdr
{
    unsigned char e_ident[16];    /* Magic number and other info */
    Elf32_Half    e_type;         /* Object file type */
    Elf32_Half    e_machine;      /* Architecture */
    Elf32_Word    e_version;      /* Object file version */
    Elf32_Addr    e_entry;        /* Entry point virtual address */
    Elf32_Off     e_phoff;        /* Program header table file offset */
    Elf32_Off     e_shoff;        /* Section header table file offset */
    Elf32_Word    e_flags;        /* Processor-specific flags */
    Elf32_Half    e_ehsize;       /* ELF header size in bytes */
    Elf32_Half    e_phentsize;    /* Program header table entry size */
    Elf32_Half    e_phnum;        /* Program header table entry count */
    Elf32_Half    e_shentsize;    /* Section header table entry size */
    Elf32_Half    e_shnum;        /* Section header table entry count */
    Elf32_Half    e_shstrndx;     /* Section header string table index */
} ;
  • unsigned char e_ident[16]

    e_ident 数组成员 意义
    e_ident[0] = 0x7f 固定的 ELF 文件魔数
    e_ident[1] = 'E' 固定的 ELF 文件魔数
    e_ident[2] = 'L' 固定的 ELF 文件魔数
    e_ident[2] = 'F' 固定的 ELF 文件魔数
    e_ident[4] 用来标识 ELF 文件类型,0 不可识别,1表示32位ELF文件格式,2表示64位ELF文件
    e_ident[5] 用来指定编码格式,0非法,1小端字节序,2大端字节序
    e_ident[6] ELF头的版本信息,0非法,1当前版本
    e_ident[7~15] 暂且没用,保留
  • e_type

    ELF 目标文件类型 取值 意义
    ET_NONE 0 未知目标文件格式
    ET_REL 1 可重定位文件
    ET_EXEC 2 可执行文件
    ET_DYN 3 动态共享目标文件
    ET_CORE 4 core文件,程序崩溃时内存映像的转储格式
    ET_LOPROC 0xff00 未知目标文件格式
    ET_HIPROC 0xff00 未知目标文件格式
  • e_machine
    e_machine 占 2 个字节,用来描述 ELF 目标文件的体系结构类型,也就是说该文件在哪种硬件平台上运行

  • e_version
    占用 4字节,表示版本信息。

  • e_entry
    占用4字节,指明操作系统运行该程序时,将控制权转交到的虚拟地址,也就是程序的入口地址

  • e_phoff
    占用 4字节,指明程序头表(program header table)在文件内的偏移量

  • e_shoff
    占用4字节,用来指明节头表(section header table)在文件内的字节偏移量

  • e_flags
    占用4字节

  • e_ehsize
    占用2字节,说明 ELF header 的字节大小

  • e_phentsize
    占用2字节,用来指明程序头表中每个条目的字节大小,即每个用来描述段信息的数据结构的大小,也就是后面要介绍的struct Elf32_Phdr

  • e_phenum
    占用2字节,用来指明程序头表中条目的数量,也就是段的个数

  • e_shentsize
    占用2字节,用来指明节头表中每个条目的字节大小,即每个用来描述节信息的数据结构的大小

  • e_shnum
    占用2字节,用来指明节头表中条目的数量,也就是节的个数

  • e_shrtrndx
    说明 string name table 在节头部表中的索引

示例代码

int main()
{
    while(1)
      ;
    return 0;
}
gcc -m32 -c main.c -o main.o
ld -m elf_i386 -Ttext 0xc0001500 -e main main.o -o main.bin

查看main.bin的二进制

内核加载器加载内核_第3张图片
main.bin.png

Elf32_Phdr

struct Elf32_Phdr 
{
    Elf32_Word p_type
    Elf32_Off  p_offset
    Elf32_Addr p_vaddr
    Elf32_Addr p_paddr
    Elf32_Word  p_filesize
    Elf32_Word  p_memsz
    Elf32_Word  p_flags
    Elf32_Word  p_align
}
  • p_type
    占用4字节,用来指明程序中该段的类型

    ELF 目标文件类型 取值 意义
    PT_NULL 0 忽略
    PT_LOAD 1 可加载程序段
    PT_DYNAMIC 2 动态链接信息
    PT_INTERP 3 动态加载器
    PT_NOTE 4 辅助信息
    PT_SHLIB 5 保留
    PT_PHDR 6 程序头表
    PT_LOPROC 0x70000000
    PT_HIPROC 0x7FFFFFFF
  • p_offset
    占用4字节,指明本段在文件内的起始偏移地址

  • p_vaddr
    占用4字节,用来指明本段在内存中的起始虚拟地址

  • p_paddr
    占用4字节,仅用于物理地址

  • p_filesize
    占用4字节,指明本段在文件中的大小

  • p_memsz
    占用4字节,用来指明本段在内存中的大小

  • p_flags
    占用4字节,用来指明本段相关的标志

    ELF 目标文件类型 取值 意义
    PF_X 1 本段具有可执行权限
    PF_W 2 本段具有可写权限
    PF_R 4 本段具有可读权限
    PF_MASKOS 0x0FF00000 本段与操作系统有关
    PF_MASKPROC 0xF0000000 本段与处理器有关
  • p_align
    占用4字节,指明本段在文件和内存中对齐方式

内核加载器加载内核_第4张图片
main.bin序,Elf32_Phdr.png

从上面的实例我们可以得得出:
【1】程序的入口地址: e_entry = 0xc0001500
【2】程序的第一段在内存中的虚拟地址: p_vaddr=0xc0001000
【3】程序的第一段大小为: p_filesz=0x00000505

内核加载器加载内核_第5张图片
三者关系.png
内核加载器实现加载内核

现在可以继续完成我们的内核加载器

;;;;;;;;;;;;;;;;;;;;;;;; 将main.bin从磁盘读入到内存中 ;;;;;;;;;;;;;;;;;;;;;
mov eax, KERNEL_START_SECTOR       ; eax = 扇区号
mov ebx, KERNEL_BIN_BASE_ADDR      ; ebx = 将磁盘读入到内存指定地址, 0x70000
mov ecx, 200                       ; ecx 读取磁盘扇区数

call rd_disk_m_32

;;;;;;;;;;;;;;;;;;;;;;; 开启内存分页 ;;;;;;;;;;;;;;;;;;
call setup_page         
......
; 在开启分页后, 重新加载gdt
lgdt [gdt_ptr]

;;;;;;;;;;;;;;;;;;;;;;; 内核初始化 ;;;;;;;;;;;;;;;;;;;;;
jmp SELECTOR_CODE: enter_kernel
enter_kernel:
    call kernel_init        ; 内核初始化

    mov esp, 0xc009f000
    jmp KERNEL_ENTRY_POINT  ; 内核代码入口地址, 0xc0001500
    
;------------------------------------------
; kernel_init 
; 功能: 内核初始化
; 参数: 无
;------------------------------------------
kernel_init:
    xor eax, eax
    xor ebx, ebx    ; ebx 将用来记录每一个(遍历)程序头表地址
    xor ecx, ecx    ; ecx 将用来记录程序头表中的元素的数量
    xor edx, edx    ; edx 将用来记录程序头表每个元素的大小, 即e_phentsize

    mov dx, [KERNEL_BIN_BASE_ADDR+42]  ; 偏移42字节是e_phentsize,表示每个program header的大小
    mov ebx, [KERNEL_BIN_BASE_ADDR+28] ; 偏移28字节是e_phoff,表示第一个program header偏移量
    add ebx, KERNEL_BIN_BASE_ADDR       ; ebx值为第一个program header偏移量
    mov cx, [KERNEL_BIN_BASE_ADDR+44]   ; cx 表示程序头表的数目
    .each_segment:
        cmp byte [ebx + 0], PT_NULL     ; p_type=PT_NULL, 忽略 
        je .PTNULL  
        
        ; 为 mem_cpy(dst, src, size) 函数调用压入参数
        push dword [ebx + 16]   ; [ebx+16] = p_filesize, 本段在文件中的大小
        mov eax, [ebx + 4]      ; [ebx+4] = p_offset, 本段在文件内的偏移量
        add eax, KERNEL_BIN_BASE_ADDR   ; eax 表示本程序段的起始地址
        push eax
        push dword [ebx + 8]    ; [ebx+8] 本段的在内存中的起始虚拟地址

        call mem_cpy
        add esp, 12
        
        .PTNULL:
        add ebx, edx    ; 设置ebx为下一个程序头表的位置 
        loop .each_segment
        
    ret

;---------------------------------------------
; mem_cpy(dst, src, size)
; 功能: 逐字节拷贝
; 参数: [ebp(esp)+8]  dst
;       [ebp(esp)+12] src
;       [ecx(esp)+16] size
;----------------------------------------------
mem_cpy:
    cld
    push ebp
    mov ebp, esp

    push ecx    ; rep指令会用到ecx, 所以将其备份

    mov edi, [ebp+8]    ; dst
    mov esi, [ebp+12]   ; src
    mov ecx, [ebp+16]   ; size
    rep movsb       
    
    pop ecx
    pop ebp

    ret

目前,低端1MB内存已用情况:


内核加载器加载内核_第6张图片
低端1MB内存已用情况.png

你可能感兴趣的:(内核加载器加载内核)