[5]加载C语言内核(从汇编世界进入C语言世界)

生成C语言程序的过程:

在这里插入图片描述
说明:.o文件是可重定位文件,重定位指的是文件里面所用的符号还没有安排地址,这些符号的地址需要将来与其他目标文件“组成”一个可执行文件时再重新定位(编排地址)

  1. 符号是调用的函数或者变量
  2. 可执行文件可由几个目标文件组成,如kernel内核多个代码文件,生成kernel.bin文件
  3. 组成即链接操作
  4. 编排地址就是对程序中的代码安排对应的地址

具体链接调试过程:

int _main(void)
{
    while(1);
    return 0;
}

main.C:链接阶段必须明确程序的入口地址,默认的entry symbol 是_start,所以主函数函数名用这个表示。
(入口地址不是代码的起始地址,因为代码开始可能是数据,非指令)

接下来gcc -m32 -c -o main.o main.c ,再ld -m elf_i386 main.o -Ttext 0xc0001500 -o kernel.bin
前面生成可重定位文件,后面可执行文件

  1. -c:生成目标文件,非可执行文件
  2. -Ttext xxx:加载到起始虚拟地址xxx处
  3. 默认入口地址用_start表示,若自定义入口地址,加 -e address说明
    ld main.o -Ttext 0xc0001500 -e _main -o kernel.bin
  4. -o指定可执行文件的名称为kernel.bin

注意:32位和64位的elf文件中有些数据结构的长度不一样,所以在编译链接中要添加类型参数-m32和-m elf_i386

2: elf格式的二进制文件

window下的可执行文件后缀是exe,但真正的格式是PE(portable executable)
Linux下可执行文件格式是ELF(后缀是out),作为在以IA32体系的不同操作系统下的可移植可执行文件。

用途:通过内核程序的elf程序头文件,loader可以获取内核需要加载到的虚拟地址,程序入口地址等和内核的相关信息。

内核被加载到内存后, loader 还要通过分析其 elf 结构将其展开到新的位置,所以说,内核在内存中有两份拷贝,一份是 elf格式的原文件 kernel.bin,另一份是 loader 解析 elf格式的 kernel.bin 后在内存中生成的内核映像(也就是将程序中的各种段 segment 复制到内存后的程序体),这个映像才是真正运行的内核

elf格式的文件头包含了程序头表(program header table)和节头表(section header table)和ELF表

  1. 程序头表中存储的是一种记录段信息的数据结构,每个成员称为条目(entry),条目对应着段的描述信息
  2. 节头表同样,多个节经过链接之后就被合并成一个段,因为cpu内存存储的是有序的段,节头表就不使用了
  3. ELF header:一个固定大小的数据结构来描述程序头表和节头表的大小及位置信息,位于文件最开始的部分,可以说是用来描述各种“头”的“头。

ELF文件格式分为文件头和文件体,链接后多节整合成段,如下图:
在这里插入图片描述

具体的头数据结构在真相还原P215

3: 内核加载过程

3.1: 内核原文件加载到0x70000处,以后会被内核映像覆盖

使用rd_disk_m_32 函数,与实模式下从硬盘读取loader到内存的方法基本一样,区别在于是32位操作数和寻址方式。

接下来准备进入分页模式(其实放在加载内核之前也可以),初始化页目录项和页表项的过程中很重要的一步是要将物理地址低端1MB映射到虚拟地址的高端3G以上,但同时要保证loader.bin的正常运行,所以虚拟地址0x0开始的区域也是低端1MB内核的数据,其他的页需要在内存管理模块进行分配。

call setup_page         ;分页机制准备工作第一步,设置页目录项和页表项
;内核放到高端地址后,相应的在loader.s创建的寄存器或者内存区域都要更改地址(实际上只是因为寻址的机制改变了,需要提供虚拟地址来转化为物理地址,所以指向的物理地址并没有变化)
sgdt [gdt_str]
mov ebx, [gdt_str+2]    ;gdt基址
or dword [ebx+0x18+4], 0xc0000000 ;视频段描述符的高4字节中段基址+0xc0000000

add dword [gdt_str+2],0xc0000000      ;gdt表移动到虚拟地址的3G以上

add esp,0xc0000000      ;栈顶地址 此时内核高端栈=0xc0000900

3.2: 内核初始化:解析elf header和program header,将所有段加载到0xc0001000处,入口地址是0xc0001500,从这里开始的内存区域是内核映像(真正的内核)

enter_kernel:
	call kernel_init          ;返回地址压栈4字节
	mov esp,0xc009f000        ;栈转移到可用区域  0xc0001000起始为内核映像,大小为60KB,栈不会破坏
	jmp kernel_entry_addr     ;测试跳转到内核映像~~~~~
;从elf header table中获取段头表的相关信息
kernel_init:
    xor eax,eax
    xor ebx,ebx
    xor edx,edx
    xor ecx,ecx
    mov dx,[kernel_base_address+42]    ;e_phentsize 程序头表中每个条目字节数
    
    mov cx,[kernel_base_address+44]    ;e_phnum 条目数即段数
    
    mov ebx,[kernel_base_address+28]    ;e_phoff 程序头表在文件中偏移
    add ebx,kernel_base_address         ;程序头表起始地址
.next_program_header:
    cmp byte [ebx+0],0                       ;程序头表第一个属性是Type
    je .PT_NULL                         ;如果为0

    ;模拟memory_cpy(destination,source,size)
    ;从$1复制$3大小的数据到$2处
    ;压入参数顺序从右向左
    push dword [ebx+16]                 ;p_filesz-size
    mov eax,[ebx+4]                     ;p_offset 本段在文件内的起始偏移字节
    add eax,kernel_base_address         
    push eax                            ;source     说明第一个段开头就是elf header等头表
    push dword [ebx+8]                  ;p_vaddr-destination本段在在内存中的起始虚拟地址		
                                        ;double word 4字节
    call mem_cpy                        ;返回地址压栈4字节
    add esp,12
.PT_NULL:
    add ebx,edx ;ebx更新为下一个程序头表项
    loop .next_program_header
    ret         ;kernel_init函数返回


mem_cpy:
    push ebp    ;压入4字节
    mov ebp,esp ;ebp获得栈顶指针信息后,可以来访问栈中元素
    push ecx    ;ecx在外循环用于记录未加载的段条目数(保护) 压入4字节
    mov edi,[ebp+8]
    mov esi,[ebp+12]
    mov ecx,[ebp+16]
    cld         ;正方向
    rep movsb   ;按字节拷贝

    pop ecx
    pop ebp
    ret

1-3行:通过每个段条目中的type信息看段的类型,如果是0即忽略该段
8-12行:为memory_cpy函数提供参数压栈
14-15行:调用完函数,要移动esp将之前的参数移除栈来保持栈稳定
22-32行:获取栈中参数的方法即ebp获取栈顶指针,移动eb
p访问数据,26-27行这里要注意es和ds需要提前加载。(在保护模式下,段寄存器是通过选择子访问GDT获取段基址,属性等,并存入不可见的高速缓冲区中)

执行结果:通过jmp kernel_entry_addr进入内核的入口虚拟地址0xc0001500,下面就是main.c函数的内容

终于实现了从汇编语言到C语言的迈进。
在这里插入图片描述
在这里插入图片描述


标题:[5]加载C语言内核(从汇编世界进入C语言世界)
作者:abandon
地址:HTTPS://www.songsci.com/articles/2021/05/25/1645758633202.html

    评论
    0 评论
avatar

取消