Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

C语言执行过程

预处理——>编译——>汇编——>链接

编译成汇编文件,再由汇编文件生成目标文件(待重定位文件)

编译

gcc -c -o kernel/main.o kernel/main.c

-c:编译汇编到目标代码,不进行链接

-o:将输出的文件以指定的文件名来存储,得到main.o文件

main.o文件是目标文件,也是待重定位文件,这个文件中调用的函数和使用的变量都没有安排编址,也没有和其它的目标文件链接成一个可执行文件。

由于不知道可执行文件由几个目标文件组成,所以目标文件中使用的符号(调用的函数或使用的变量)一律在链接阶段进行编址。

链接

ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin

ld 是链接,将目标文件链接编址

-Ttext:指定程序的入口地址,表示程序将从哪里开始执行

-e:指定起始执行的名

-o:将文件输出为kernel.bin

目标文件(待重定位文件)

汇编后生成的目标文件,在该目标文件中,如果引用了其它外部文件中定义的符号(变量或者函数),在编译阶段只能标出一个符号名,该符号的具体地址还不确定,因为还不知道这个符号在哪个外部文件中,该外部文件需要在重定位后才能确定地址,而重定位需要在链接阶段完成的。

可执行文件

Windows下可执行文件是:PE

Linux下可执行文件是:elf(可执行链接格式)

gcc编译后生成elf文件的头目的是什么

每个程序是单独存在的,程序的入口地址信息需要和程序绑定,在程序的头文件中写入入口地址,主调函数调用后将其加载到入口地址处,然后跳转过去执行。

大体上粗糙的看,一个程序被编译链接成可执行文件后,会生成一个文件头,文件头后面才是写的程序。文件头中指定了程序起始地址和程序的大小。文件头中指定的程序的入口地址,也就是这个程序开始执行的地址。操作系统在加载的时候读到文件头信息,知道其入口地址,也获取了该程序的大小,那么就将这个程序载入入口地址为其文件头指定的入口地址,然后跳转过去执行。注意也必须要知道这个程序的大小,因为每个进程都有自己的页表,这个页表包括程序的所有数据(代码段,数据段,堆栈)载入时需要给这个进程按照页表大小的粒度给其分配内存,然后写他的内存的bitmap。

ELF(Executable and Linkable Format)是Linux下可执行二进制文件

ELF文件格式

在链接时会给程序编址,也要指定程序的入口地址,为了防止在加载程序时加载地址不要写死,将程序的入口地址写在程序的头文件中,在加载程序的时候读出程序的入口地址,然后将程序加载到入口地址处,然后跳转到程序的入口地址处执行。

img

程序头只是元信息,它不是可执行性代码,操作系统将这种具有程序头格式的程序文件从外存读到内存后,从该程序的头文件中读出入口地址,需要跨过程序头直接跳转到入口处执行。

ELF header就是用一个固定大小的数据结构存储程序头表(program header table)和节头表(section header),一个可执行文件开头的部分一定是 elf header。

注意:在elf头中规定了程序的入口地址,指明了操作系统在运行该程序时,将控制权转交到虚拟地址。Elf头在一个文件中的规定位置,其大小也是固定的。Elf头中还有一些重要信息,是用来描述段头表的,段头表也是表,其中存放的是各个段的信息,各个段的信息重要的包含了该段在内存中的大小,该段在内存中的虚拟起始地址。因为elf格式是通过gcc , ld后生成的,ld连接时就指定了程序的入口地址,ld后生成的文件是在磁盘上的,需要将这个可执行文件(带有文件头的文件)加载到内存中,然后分析文件头中的信息,具体做法是:在elf可执行文件中,将段头表的信息读取出来(主要是段在内存的大小和段在内存的起始虚拟地址),将这个段加载到其指定的位置即可。

ELF Header中规定了文件类型,入口地址,编码方式(大端、小端),体系结构类型,Program Header大小和偏移, sector Header大小和偏移。

Program Header中描述了位于磁盘上文件中的段(数据段代码段等),说明本段在文件中的偏移,在内存中的虚拟地址,段的大小等。

img

ELF的生成过程

*step1:预处理*

处理头文件、宏、注释等,生成 .i 文件

*step2:编译*

通过编译器gcc将文件编译成汇编代码, 生成 .S 文件

*step3:汇编*

通过汇编器 as 将汇编代码转换为机器指令, 生成目标文件 .o 文件(可重定位文件)

*step4:链接*

通过链接器 ld 将多个目标文件和库文件,生成 ELF 文件(可以直接执行的文件)

当多个可重定位文件链接成一个可执行文件时,计算机如何知道程序的入口地址在哪?程序内的地址是在链接阶段编址的,所以在链接阶段必须要明确入口地址,连接器规定默认把名为_start的函数作为程序的入口地址,除非特别指定

操作系统加载ELF文件

【由链接器生成的ELF文件首先会存储在磁盘上,然后由操作系统在程序执行时将其载入内存】

step1:系统调用 execve() 将elf文件加载进内存

step2:解析elf 文件, 读取elf 文件头部(elf header),验证文件类型和权限,检查程序头表(Program Headers),确定需要加载的段,如代码段.text、数据段.data

step3:分配内存,将代码段和数据段映射到内存指定地址

step4:如果有动态链接,加载动态库到内存

step5:设置堆和栈参数

step6:跳转到入口地址执行

程序中多个节section经过链接后变成segment,segment就是程序中的代码段、数据段等,多个section组成段。一个程序中的segment和section的大小和数量都是未知的,所以需要一个数据结构描述他们,就像GDT那样。

程序头表(program header table):描述程序头

节头表(section header table):描述节头

Elf格式:位于文件最开始位置并且具有固定大小的数据结构,描述程序头表和节头表,它主要作用于链接阶段和运行方面。

Elf header结构

img

操作系统加载器是如何加载内核文件的呢?

Loader加载内核

Step1:加载内核:把内核文件(kernel.bin)加载到内核缓冲区

Step2:初始化内核:在分页后将加载进来的elf内核文件安置到相应的虚拟内存中,然后跳过去执行,loader的任务结束。【loader分析elf格式的kernel.bin文件,并将其展开到新的位置,这才是真正的内核】

****注意:****从磁盘加载进内存的elf格式的kernel.bin文件,需要再次经过loader解析,将其按照elf的内容展开到新的地址空间,展开后的kernel才是真正的内核映像,展开后最初的kernel.bin就没有用了,然而内核是需要放在低1MB内存空间的,所以最初的kernel.bin文件要尽可能放在高位置,因为被覆盖了也没关系,展开后的kernel尽量放在低位置,它可以有足够的空间向上增长

评论