文章目录
- 编译和链接【一】
- 从翻译单元到二进制文件
编译和链接【一】
在我大一的时候, 我使用VC6.0对C语言程序进行编译链接和运行 , 然后我接触了VS, VS code等众多IDE, 这些IDE界面友好, 使用方便, 例如我最喜欢的VS,一键编译运行。对于大一的我,不需要了解编译的整个过程就可以运行,这无疑是非常棒的,并且增加了我对编程的兴趣,同时也简化了我后续的软件开发, 我只需要关心业务和功能代码即可。
但是今天, 我不想“逃课了”,欢迎来到我的频道,本节将会介绍编译中的一系列细节。
在正式开始之前,我要推荐两本书,一本是《程序员的自我修养》,另一本是《鲸书》,这两本书对编译的整个过程做了非常详细,非常完备的介绍,但是恰恰如此,我想很多时候,很多知识在工作上是用不到的,也许这句话在很多年多的我会反驳,但是站在工作一年的现在,我将会给你介绍,我所了解的编译和链接。
从翻译单元到二进制文件
在程序的编译过程中,其实就是把我们所写的代码翻译成CPU能够识别和运行的二进制机器指令,具体指令需要查阅相关架构的指令集,如x86,Arm等。
这里给出一个简单的源程序
header.h
#pragma once#include <string> void say(std::string);
source.cpp
#include "header.h"#include <iostream>void say(std::string ctx)
{std::cout << ctx << std::endl;
}
main.cpp
#include "header.h"#include <iostream>using namespace std;int main()
{say("hello");return 0;
}
在上面的程序中,我们有三个文件:header.h, source.cpp, main.cpp。
在main.cpp中,我们定义的项目的入口函数int main(), 在这个函数里,我们调用了say函数, 而这个函数在header.h文件中声明。
在编译的时候, 编译器会检查我们源程序的语法,例如:参数类型,返回结果,函数签名等,这里有一个c++的name mangling规则,以后会详细解释。
以上就是一个典型的C++程序,我们可以把source.cpp和header.h视为某个模块或者某个功能库,其他模块想要调用这个say函数,则要先包含头文件即可,在Ubuntu上,编译指令为:
g++ main.cpp source.cpp
我们在终端上查看可执行文件的信息
查看文件的section header
典型的段有: 代码段,数据段,BSS段和只读段等,每个section都有一个section header来描述,其中包括:段名,类型,起始地址,段偏移和大小。
将这些section headers集中在一起,就是section header table,也就是所谓的节头表,通过节头表,可以看出一个可执行文件的基本构成,当然,还有linux系统必备的ELF header,用来描述文件类型,运行的平台,入口地址等。当程序运行的时候,加载器 会根据这些此文件头来获取可执行文件的信息。
而所谓的编译,就是将源程序的函数、变量等信息分类组合,放在各个段中,比如说:字符串常量放在只只读数据段(rodata),如果编译时,添加debug信息,那么还会有一个debug section,用来保存可执行文件中每一条二进制指令对应的源码位置信息,GDB就可以根据这些信息来进行调试,最后,编译器还会添加一些额外的seciont,比如说init 段,用来初始化运行库的汇编代码,程序运行所需要的环境。
在一个大型C/C++项目里,编译器是以C/C++的源文件为翻译单元,进行编译的,在编译的不同阶段,编译程序会使用不同的工具来完成不同的阶段,例如:预处理器,编译器,汇编器,链接器。
- 预处理器:将源程序main.c经过预处理为main.i
- 编译器:将main.i编译为汇编文件main.s
- 汇编器:将main.s编译为目标文件main.o
- 链接器:将各个目标文件main.o, souce.o链接成可执行文件a.out
最后生成的可执行文件a.out其实也是目标文件的一种,唯一不同的是,a.out是可执行的目标文件,而其余的目标文件有:
- 可重定位的目标文件:main.o, source.o
- 可执行的目标文件:a.out
- 可被共享的目标文件:source.so
汇编器生成的目标文件是可重定位的目标文件,是不可执行的,需要经过链接器链接和重定位之后才能运行。
可被共享的目标文件一般是共享库,在程序运行时动态地加载到内存,跟应用程序一起运行。
关注我,下期继续介绍编译和链接