作者:我是王灿_246 | 来源:互联网 | 2023-10-17 12:43
程序是怎样运行的写好的c程序怎样运行的呢?答案是一个写好的程序要先经过语言预处理器,编译器,汇编器和链接器生成最后的可执行文件,然后加载器将可执行文件加载到内存中才能运行。这里以一
程序是怎样运行的
写好的c程序怎样运行的呢?答案是一个写好的程序要先经过语言预处理器,编译器,汇编器和链接器生成最后的可执行文件,然后加载器将可执行文件加载到内存中才能运行。
这里以一个c程序main.c和GNU编译系统(静态链接)为例来说明生成可执行文件的过程:
1.首先经过c预处理器(cpp)将main.c翻译成一个 ASCII码的中间文件main.i;
2.接下来驱动程序运行C编译器(cc1)将main.i翻译成一个ASCII汇编语言文件main.s;
3.随后驱动程序运行汇编器,将main.s(as)翻译成一个可重定位目标文件main.o;
4.最后运行链接器ld将main.o和其他的一些的可重定位目标文件链接在一起创建一个可执行目标文件。
下面说一下程序运行之前每个阶段的所做的具体工作:
1.预处理阶段:预处理器cpp根据以字符#开头的命令,修改原始的C程序。比如常见的命令
#include
就是告诉编译器读取系统头文件stdio.h的内容,这个头文件里包含了一些可能用到的函数的声明,预处理器将它直接插入到程序文本中,就得到了另一个c程序——main.i
2.编译阶段:编译器(cc1)将文本文件main.i翻译成文本文件翻译成文本文件mian.s,它包含了 一个汇编语言程序,汇编语言程序中的每条语句都描述了一句机器指令。汇编语言是非常有用的,因为它为不同的高级语言的不同编译器提供了通用的输出语言。
3.汇编阶段:接下来汇编器(as)将main.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标文件的格式,即main.o。它是一个二进制文件。
4.链接阶段:假设main.o调用了printf函数,而printf函数是标准C库中的一个函数,位于一个名为printf.o的单独预编译好了的目标文件中,问这个文件必须以某种方式合并到main.o中去,链接器(ld)就负责处理这种合并,结果得到main文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行。
5.加载:在命令行上键入
./main
外壳程序会调用操作系统中一个叫做加载器的函数,它拷贝可执行文件中的代码和数据到存储器中,并将控制权转移到程序的开头。
什么是可重定位文件
那么什么是可重定位目标文件呢,它的结构又是什么样子呢?
接下来是一张图说明了在Unix系统下的elf格式的可重定位目标文件的结构
下面来解释一下每个部分作用。
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分暂时可以先不理会。
加载ELF头和节头部表之间的都是节。
.text:已编译程序的机器代码
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表
.data:已初始化的全局C变量
.bss:未初始化的全局C变量,注意这个节不占据实际的空间,仅仅是一个占位符,因为未初始化的全局变量初始值为0,没有保存的必要,这样做可以更好地节省空间。
.symtab:一个符号表,他存放在程序中定义和引用的函数和全局变量的信息。
剩下的节先不说,这里说一下关于符号表相关的内容。
首先吐槽一下csap这本书的翻译,实在是含糊不清,有些词比如引用,全局等容易引起歧义的词太多,还有几处翻译错误如果英文好的直接看原版可能更好理解吧…..
首先”符号”这个词就很令人困惑,其实符号就是定义的函数名或者变量名(局部变量不在符号表上,局部变量运行时在栈上被管理,链接器不管)
这一章所用的”全局”也有歧义,这一章的”全局符号”是指不带static修饰的全局变量,也就是说能在所有模块中使用的变量,这其中又分为在本地定义的”全局符号”和在其他模块中定义的外部(extern)全局符号。
在本地定义的带static修饰的全局变量被称为本地符号。
以上这三种(两种全局符号和一种本地符号)就是符号表中的内容,但是还有一种特例,那就是带static修饰的局部变量, 它不是在栈中管理,而是由编译器在.data和.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。比如说以下这种情况
int f()
{
static int x = 0;
return x;
}
int g()
{
static int x = 1;
return x;
}
在这种情况下,编译器引出两个唯一的本地链接器符号给汇编器。比如它可以用x.1表示函数f中的定义,用x.2表示函数g中的定义。其中x.1存储在.bss中,x.2存储在.data中。
既然说到了static,不妨说一下它的作用,刚才说了用static修饰的全局变量只能在本地被访问,这个就好像面向对象里的private的作用,所以可以用static属性隐藏变量和函数名字。