编译链接浅析

引子

先来看两段代码,这两段代码很简单。main是主函数,调用sum进行求和,其中sum函数参数是指针类型的,也就是说在sum函数进行改变会引起原来的值的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//main.cpp
#include<iostream>

using namespace std;

int sum(int* a,int* b);

int main(int argc, const char** argv) {
int k=-2;
int j=-5;
int* a;
int* b;
a = &k;
b = &j;
cout<<"k+j="<<sum(a,b)<<endl;
cout<<"k="<<k<<"\t"<<"j="<<j<<endl;
return 0;
}

1
2
3
4
5
6
7
8
//sum.cpp
#include<math.h>

int sum(int *a,int *b)
{
*a = abs(*a);*b=abs(*b);
return (*a+*b);
}

如果需要在Linux上面执行出结果,那么打上下面的代码就可以了:

1
2
g++ main.c sum.cpp -o a.out
./a.out
输出的结果是:
1
2
k+j=7
k=2 j=5

可以看到运行正确了,这个结果也是预料之中的,k 变成了 2,j 变成了 5,这个是在sum里面被改变了值,也会影响到原址的值。

程序到可执行文件

程序到可执行文件需要经过几个阶段:

预编译阶段

预编译阶段是把原始的代码文件的需要预编译头加进来,即对于#include的文件进行原样复制,#include文件可以由命令locate得到

1
2
3
4
5
6
7
8
9
10
11
ubuntu@ubuntu:~/VSCode/CSAPP深入理解计算机系统$ locate iostream
/usr/include/c++/7/iostream
/usr/lib/x86_64-linux-gnu/libboost_iostreams.so.1.65.1
/usr/share/doc/libboost-iostreams1.65.1
/usr/share/doc/libboost-iostreams1.65.1/changelog.Debian.gz
/usr/share/doc/libboost-iostreams1.65.1/copyright
/usr/share/lintian/overrides/libboost-iostreams1.65.1
/var/lib/dpkg/info/libboost-iostreams1.65.1:amd64.list
/var/lib/dpkg/info/libboost-iostreams1.65.1:amd64.md5sums
/var/lib/dpkg/info/libboost-iostreams1.65.1:amd64.shlibs
/var/lib/dpkg/info/libboost-iostreams1.65.1:amd64.triggers
或者是对于C语言的库使用man
1
man stdio
1
2
3
4
5
6
7
8
9
10
11
12
13
STDIO(3)                   Linux Programmer's Manual                  STDIO(3)

NAME
stdio - standard input/output library functions

SYNOPSIS
#include <stdio.h>

FILE *stdin;
FILE *stdout;
FILE *stderr;

DESCRIPTION
预编译命令如下:
1
2
g++ -E main.cpp -o main.i
cat main.i
这个命令是把main.cpp\(\rightarrow\)main.i
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//部分代码,在Linux试一下就知道
namespace std __attribute__ ((__visibility__ ("default")))
{

# 60 "/usr/include/c++/7/iostream" 3
extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;


extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;




static ios_base::Init __ioinit;


}
# 2 "main.cpp" 2


# 3 "main.cpp"
using namespace std;

int sum(int* a,int* b);

int main(int argc, const char** argv) {
int k=-2;
int j=-5;
int* a;
int* b;
a = &k;
b = &j;
cout<<"k+j="<<sum(a,b)<<endl;
cout<<"k="<<k<<"\t"<<"j="<<j<<endl;
return 0;
}
可以看到,这就是把#include里面的东西原样复制到main.cpp代码文件里面,整合到一起,就叫做预编译过程。 ### C++编译阶段 C++编译阶段就是把预编译好的文件编译成汇编代码,个人觉得这个过程跟翻译差不多,把一种语言翻译成另一种语言,命令如下。
1
2
g++ -S main.i
cat main.s
上面的g++命令默认会有-o main.s添加上,即会产生main.s文件产生。 产生的main.s文件如下,由于太长,截取了部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
	.file	"main.cpp"
.text
.section .rodata
.type _ZStL19piecewise_construct, @object
.size _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
.zero 1
.local _ZStL8__ioinit
.comm _ZStL8__ioinit,1,1
.LC0:
.string "k+j="
.LC1:
.string "k="
.LC2:
.string "\t"
.LC3:
.string "j="
.text
.globl main
.type main, @function
main:
.LFB1493:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %rbx
subq $56, %rsp
.cfi_offset 3, -24
movl %edi, -52(%rbp)
movq %rsi, -64(%rbp)
movq %fs:40, %rax
movq %rax, -24(%rbp)
xorl %eax, %eax
movl $-2, -48(%rbp)
movl $-5, -44(%rbp)
leaq -48(%rbp), %rax
movq %rax, -40(%rbp)
leaq -44(%rbp), %rax
movq %rax, -32(%rbp)
leaq .LC0(%rip), %rsi
leaq _ZSt4cout(%rip), %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
movq %rax, %rbx
movq -32(%rbp), %rdx
movq -40(%rbp), %rax
movq %rdx, %rsi
movq %rax, %rdi
call _Z3sumPiS_@PLT
movl %eax, %esi
movq %rbx, %rdi
call _ZNSolsEi@PLT
movq %rax, %rdx
movq

从上面的代码看出,这是个汇编代码,也就完成了main.i\(\rightarrow\)main.s的转化。

汇编代码转化为rof文件

rof是relocatable object file的简称,称为可重定向目标文件。

1
g++ -c main.s

输出的文件为main.o,这个文件不是文本文件,是一个二进制文件。

文本文件:由ASCII码组成的文件,可以由文本编辑器直接打开 二进制文件:除了文本文件之外的所有文件,如图片格式文件就属于二进制文件,这种文件需要专门的解码软件打开,如果使用文本编辑器打开就是乱码的。

链接器链接生成eof

eof是executable object file的简称,称为可执行目标文件。

按照前面的步骤(预编译\(\rightarrow\)C++编译\(\rightarrow​\)rof文件生成)得到sum.o文件。然后进行:

1
g++ main.o sum.o

这时候会产生一个新的文件为a.out,这个名字可以自己取,也就是把默认的那个语句加上,如:

1
g++ main.o sum.o -o main.out
当然,在 Linux 里面文件的后缀名是不重要的,.x也可,只不过.out是在Unix操作系统上第一个实现的可执行文件的后缀名,就保留了下来。

之后就运行:

1
./a.out
得到结果
1
2
k+j=7
k=2 j=5
这个结果跟之前的一致。执行命令的时候,Linux 会使用它的加载器将可执行文件a.out的代码和数据复制到内存,然后将控制转移到这个程序的开头。

小结

可执行文件格式分析