-
操作系统如何加载二进制程序?
操作系统本身是一个程序,操作系统加载程序可以简单看成: 用一个程序去调用另一个程序。最简单的做法就是jmp
或call
指令,比如我们的BIOS就是这样调用mbr,mbr再调用loader,但之前调用方法是很不灵活的,比如mbr的地址是0x7c00,loader的地址是0x900。这2个加载地址是固定的,调用方需要和被调用方提前约定加载地址。操作系统要加载那么多的程序,是不可能一一约定的。有没有一种方法让加载地址不那么固定?
由于每个程序是单独存在的,所以程序的入口地址信息需要与程序绑定,最简单的方法就是在程序文件中专门腾出个空间来写入程序的入口地址。主调程序在该程序文件的相应空间中将该程序的入口信息读出来,将其加载到相应的入口地址,再跳转过去执行。(我们要实现的"内核加载器加载内核",大概思想也是如此),当然不仅仅是写程序的入口地址,也需要写很多东西,最后这些东西就构成了 程序头。于是程序就有 "程序头" + "程序体"。
elf格式的二进制文件
ELF指的是,可执行链接格式,它是应用程序的二进制接口。
ELF目标格式文件类型:
- 待重定位文件
待重定位文件属于源文件编译后但未完成链接的半成品,它被用于与其他目标文件合并链接,一构建出二进制可执行文件或动态链接库。
为什么称其为待重定位呢?
因为在该目标文件中,如果引用了其他外部文件(其他目标文件或库文件)中定义的符号(全局变量或者全局函数等),在编译阶段只能先标识出一个符号名,该符号的地址还不能确定,因为不知道该符号是在哪个外部文件中,该外部文件需要被重定位后才能确定文件内的符号地址,这个重定位的工作是在链接过程中完成的。 - 共享目标文件
在可执行文件被加载的过程中被动态链接,成为程序代码的一部分。 - 可执行文件
进过编译链接后的,可以直接运行的程序文件。
程序中最重要的部分就是段和节,它们是真正的程序体。段比如代码段和数据段等,同样也有很多节,段是由节组成的,多个节经过链接之后就被合并成一个段了。
段和节的起始地址和大小信息是在用header中描述的。分别是程序头表(program header table)和节头表(section header table)。里面描述的是程序头和节头信息。在这2个表中,每个元素称之为条目。
ELF格式的作用体现在2个方面: 一是链接阶段,二是运行阶段
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_Phdre_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的二进制
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字节,指明本段在文件和内存中对齐方式
从上面的实例我们可以得得出:
【1】程序的入口地址: e_entry = 0xc0001500
【2】程序的第一段在内存中的虚拟地址: p_vaddr=0xc0001000
【3】程序的第一段大小为: p_filesz=0x00000505
内核加载器实现加载内核
现在可以继续完成我们的内核加载器
;;;;;;;;;;;;;;;;;;;;;;;; 将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内存已用情况: