C++源文件到可执行程序

文章作者:Tyan
博客:noahsnail.com  |  CSDN  |  简书

1. 引言

C++程序从源代码到可执行程序是一个复杂的过程,其流程为:源代码 --> 预处理 --> 编译 --> 优化 --> 汇编 --> 链接 --> 可执行文件,本文以一段C++代码为例,按执行顺序来描述这个过程。

2. 源代码

源代码文件分为两个,hello.hhello.cppmain.cpp,代码如下:

  • hello.hpp

    1
    2
    3
    4
    5
    6
    #ifndef HELLO_HPP_
    #define HELLO_HPP_

    void hello();

    #endif
  • hello.cpp

    1
    2
    3
    4
    5
    6
    7
    #include "hello.hpp"
    #include <iostream>
    using namespace std;

    void hello() {
    cout << "Hello, world!" << endl;
    }
  • main.cpp

    1
    2
    3
    4
    5
    6
    #include "hello.hpp"

    int main(int argc, char *argv[]) {
    hello();
    return 0;
    }

3. 预处理

预处理是指C++程序源代码在编译之前,由预处理器(Preprocessor)对C++程序源代码进行的处理。在这个阶段,预处理器会处理以#开头的命令,处理完成之后会生成一个不包含预处理命令的纯C++文件,常见的预处理有:文件包含(#inlcude)、条件编译(#ifndef #ifdef #endif)、提供编译信息(#pragma)、宏替换(#define)等。

使用g++预处理main.cpp的命令如下:

1
[root@localhost:/workspace] $: g++ -E main.cpp -o main.ii

-E参数表示预处理后即停止,不进行编译,预处理后的代码送往标准输出,-o指定输出文件。输出文件main.ii的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "main.cpp"
# 1 "hello.hpp" 1



void hello();
# 2 "main.cpp" 2

int main(int argc, char *argv[]) {
hello();
return 0;
}

4. 编译

在编译过程中,编译器主要作语法检查和词法分析。通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

编译main.ii的命令如下:

1
[root@localhost:/workspace] $: g++ -S main.ii

-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
        .file   "main.cpp"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
call _Z5hellov
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)"
.section .note.GNU-stack,"",@progbits

5. 优化

优化是在编译过程中最重要的,也是最难的。它不仅与编译技术本身有关,而且跟机器的硬件环境也有很大的关系。优化可在编译的不同阶段进行,一类优化是对中间代码的优化,这类优化不依赖于具体的计算机,另一类优化是对目标代码的优化,这类优化与机器的硬件环境有关。

g++编译器的编译优化参数为-O,分为四级,分别为-O0-O1-O2-O3,默认为-O0。各级优化后的结果如下:

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
# 默认优化,-O0
[root@localhost:/workspace] $: g++ -c main.cpp hello.cpp
[root@localhost:/workspace] $: nm -C main.o
U __cxa_atexit
U __dso_handle
000000000000007a t _GLOBAL__sub_I__Z5hellov
0000000000000022 T main
000000000000003d t __static_initialization_and_destruction_0(int, int)
0000000000000000 T hello()
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)


# 优化级别-O1
[root@localhost:/workspace] $: g++ -c -O1 main.cpp hello.cpp
[root@localhost:/workspace] $: nm -C main.o
U __cxa_atexit
U __dso_handle
000000000000007d t _GLOBAL__sub_I__Z5hellov
000000000000006a T main
0000000000000000 T hello()
U std::ctype<char>::_M_widen_init() const
U std::ostream::put(char)
U std::ostream::flush()
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
U std::__throw_bad_cast()
U std::cout
0000000000000000 b std::__ioinit

# 优化级别-O2
[root@localhost:/workspace] $: g++ -c -O2 main.cpp hello.cpp
[root@localhost:/workspace] $: nm -C main.o
U __cxa_atexit
U __dso_handle
0000000000000010 t _GLOBAL__sub_I__Z5hellov
0000000000000000 T main
0000000000000000 T hello()
U std::ctype<char>::_M_widen_init() const
U std::ostream::put(char)
U std::ostream::flush()
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
U std::__throw_bad_cast()
U std::cout
0000000000000000 b std::__ioinit

6. 汇编

汇编是把汇编语言代码翻译成目标机器指令的过程。

编译main.s的命令如下:

1
[root@localhost:/workspace] $: g++ -c main.s

-c参数表示编译或汇编源文件,但是不作连接,编译器输出对应于源文件的目标文件。输出文件为main.o,使用nm -C main.o命令来查看文件内容,文件内容如下:

1
2
0000000000000000 T main
U hello()

7. 链接

链接是将目标文件、启动代码、库文件链接成可执行文件的过程,得到的文件可以直接执行。经过汇编之后生成的目标文件main.o是不可以直接执行的。链接命令如下:

1
2
3
4
[root@localhost:/workspace] $: g++ main.o -o main
main.o: In function `main':
main.cpp:(.text+0x10): undefined reference to `hello()'
collect2: error: ld returned 1 exit status

从上面可以看出,只链接main.o文件会报错,这是因为main.cpp引用了hello.cpp中定义的函数hello,因此需要链接文件hello.cpp才能生成可执行程序。重复上述过程,生成hello.o,链接两个文件的命令如下:

1
[root@localhost:/workspace] $: g++ main.o hello.o -o main

经过链接,多个文件被链接成了单一的可执行文件main,执行main程序:

1
2
[root@localhost:/workspace] $: ./main
Hello, world!

7.1 静态链接库

除了直接链接多个目标文件之外,还可以通过链接静态库生成可执行文件。静态链接库是编译器生成的一系列对象文件的集合,库中的成员包括普通函数,类定义,类的对象实例等。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。可执行文件生成之后,就不再需要静态链接库,即编译后的可执行程序不需要外部函数库的支持。但如果静态链接库发生改变,则可执行程序需要重新编译。静态链接库属于编译时链接。

我们再添加两个static.hppstatic.cpp,并修改main.cpp,内容如下:

  • static.hpp文件:

    1
    2
    3
    4
    5
    6
    #ifndef STATIC_HPP_
    #define STATIC_HPP_

    void test();

    #endif
  • static.cpp文件:

    1
    2
    3
    4
    5
    6
    7
    #include "static.hpp"
    #include <iostream>
    using namespace std;

    void test() {
    cout << "static lib" << endl;
    }
  • main.cpp文件:

1
2
3
4
5
6
7
8
extern void hello();
extern void test();

int main(int argc, char *argv[]) {
hello();
test();
return 0;
}

编译汇编hello.cppstatic.cpp之后可以得到两个文件hello.ostatic.o,linux系统中的命令ar,可以将多个目标文件打包成为一个单独的文件,这个文件被称为静态库。生成静态库的命令如下:

1
2
[root@localhost:/workspace] $: ar -r libstatic.a hello.o static.o
ar: creating libstatic.a

查看libstatic.a的内容:

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
[root@localhost:/workspace] $: nm -C libstatic.a

hello.o:
U __cxa_atexit
U __dso_handle
000000000000005f t _GLOBAL__sub_I__Z5hellov
0000000000000022 t __static_initialization_and_destruction_0(int, int)
0000000000000000 T hello()
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

static.o:
U __cxa_atexit
U __dso_handle
000000000000005f t _GLOBAL__sub_I__Z4testv
0000000000000022 t __static_initialization_and_destruction_0(int, int)
0000000000000000 T test()
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

通过静态链接库生成可执行程序main并执行:

1
2
3
4
[root@localhost:/workspace] $: g++ main.o libstatic.a -o main
[root@localhost:/workspace] $: ./main
Hello, world!
static lib

另一种命令方式:

1
[root@localhost:/workspace] $: g++ -L ./ main.cpp -lstatic -o main

Linux静态库的命名惯例是名字以三个字母lib开头并以後缀.a结束。所有的系统库都采用这种命名惯例,并且它允许通过-l(ell)选项来简写命令行中的库名。-lstatic中的-l是要求编译器在系统库目录下查找static库,staticlibstatic.a的简写。-L参数用来指定要具体的查找目录,如果缺少这个参数,则只会在系统库目录下查找static,会报错。错误如下:

1
2
[root@localhost:/workspace] $: g++ main.cpp -lstatic -o ltest
/usr/bin/ld: cannot find -lstatic

7.2 共享库

共享库(Windows叫动态链接库)是编译器以一种特殊的方式生成的对象文件的集合。对象文件模块中所有地址(变量引用或函数调用)都是相对而不是绝对的,这使得共享模块可以在程序的运行过程中被动态地调用和执行。共享库属于运行时链接。当使用共享库时,只要共享库的接口不变,共享库修改之后,不需要重新编译可执行程序。

创建dynamic.cpp,内容如下:

1
2
3
4
5
6
#include <iostream>
using namespace std;

void test() {
cout << "dynamic lib" << endl;
}

编译hello.cppdynamic.cpp-fpic表示生成的对象模块采用浮动(可重定位)地址,pic是位置无关代码(position independent code)的缩写。:

1
[root@localhost:/workspace] $: g++ -c -fpic hello.cpp static.cpp

使用-fpic与不使用-fpic生成的目标文件hello.o

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
# 使用-fpic
U __cxa_atexit
U __dso_handle
U _GLOBAL_OFFSET_TABLE_
0000000000000076 t _GLOBAL__sub_I_hello.cpp
000000000000002e t __static_initialization_and_destruction_0(int, int)
0000000000000000 T hello()
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

# 不使用-fpic
U __cxa_atexit
U __dso_handle
000000000000005f t _GLOBAL__sub_I__Z5hellov
0000000000000022 t __static_initialization_and_destruction_0(int, int)
0000000000000000 T hello()
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
U std::ios_base::Init::Init()
U std::ios_base::Init::~Init()
U std::cout
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

创建共享库dynamic.so-shared表示生成共享目标文件。:

1
[root@localhost:/workspace] $: g++ -shared hello.o dynamic.o -o libdynamic.so

编译main.cpp并链接共享库:

1
[root@localhost:/workspace] $: g++ main.cpp libdynamic.so -o main

执行main

1
2
[root@localhost:/workspace] $: ./main
./main: error while loading shared libraries: dynamic.so: cannot open shared object file: No such file or directory

报错是因为当前工作目录可能不在共享库的查找路径中,因此需要将当前目录添加到环境变量LD_LIBRARY_PATH中:

1
2
3
4
[root@localhost:/workspace] $: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
[root@localhost:/workspace] $: ./main
Hello, world!
dynamic lib

查看链接静态库和共享库生成的两个可执行main文件:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# 共享库
[root@localhost:/workspace] $: nm -C main
000000000060103c B __bss_start
000000000060103c b completed.6354
0000000000601038 D __data_start
0000000000601038 W data_start
0000000000400650 t deregister_tm_clones
00000000004006c0 t __do_global_dtors_aux
0000000000600dd8 t __do_global_dtors_aux_fini_array_entry
00000000004007b8 R __dso_handle
0000000000600de8 d _DYNAMIC
000000000060103c D _edata
0000000000601040 B _end
00000000004007a4 T _fini
00000000004006e0 t frame_dummy
0000000000600dd0 t __frame_dummy_init_array_entry
00000000004008e8 r __FRAME_END__
0000000000601000 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
00000000004005a8 T _init
0000000000600dd8 t __init_array_end
0000000000600dd0 t __init_array_start
00000000004007b0 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000600de0 d __JCR_END__
0000000000600de0 d __JCR_LIST__
w _Jv_RegisterClasses
00000000004007a0 T __libc_csu_fini
0000000000400730 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
000000000040070d T main
0000000000400680 t register_tm_clones
0000000000400620 T _start
0000000000601040 D __TMC_END__
U test()
U hello()


# 静态库
[root@localhost:/workspace] $: nm -C main
000000000060105c B __bss_start
0000000000601170 b completed.6354
U __cxa_atexit@@GLIBC_2.2.5
0000000000601058 D __data_start
0000000000601058 W data_start
00000000004007b0 t deregister_tm_clones
0000000000400820 t __do_global_dtors_aux
0000000000600de8 t __do_global_dtors_aux_fini_array_entry
0000000000400a08 R __dso_handle
0000000000600df8 d _DYNAMIC
000000000060105c D _edata
0000000000601178 B _end
00000000004009f4 T _fini
0000000000400840 t frame_dummy
0000000000600dd0 t __frame_dummy_init_array_entry
0000000000400c40 r __FRAME_END__
0000000000601000 d _GLOBAL_OFFSET_TABLE_
0000000000400960 t _GLOBAL__sub_I__Z4testv
00000000004008ec t _GLOBAL__sub_I__Z5hellov
w __gmon_start__
00000000004006d0 T _init
0000000000600de8 t __init_array_end
0000000000600dd0 t __init_array_start
0000000000400a00 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000600df0 d __JCR_END__
0000000000600df0 d __JCR_LIST__
w _Jv_RegisterClasses
00000000004009f0 T __libc_csu_fini
0000000000400980 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
000000000040086d T main
00000000004007e0 t register_tm_clones
0000000000400780 T _start
0000000000601060 D __TMC_END__
00000000004008af t __static_initialization_and_destruction_0(int, int)
0000000000400923 t __static_initialization_and_destruction_0(int, int)
0000000000400901 T test()
000000000040088d T hello()
U std::ostream::operator<<(std::ostream& (*)(std::ostream&))@@GLIBCXX_3.4
U std::ios_base::Init::Init()@@GLIBCXX_3.4
U std::ios_base::Init::~Init()@@GLIBCXX_3.4
0000000000601060 B std::cout@@GLIBCXX_3.4
U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)@@GLIBCXX_3.4
0000000000601171 b std::__ioinit
0000000000601172 b std::__ioinit
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@@GLIBCXX_3.4

8. 可执行文件

可执行文件指的是可以由操作系统进行加载执行的文件。在不同的操作系统环境下,可执行程序的呈现方式不一样。例如上面生成的main就是Linux系统下的可执行文件,windows系统下的可执行文件一般为*.exe

参考资料

  1. https://wiki.ubuntu.org.cn/Compiling_Cpp
  2. https://tech.meituan.com/2015/01/22/linker.html
  3. http://notes.maxwi.com/3416/06/05/source-to-program/
  4. http://www.ruanyifeng.com/blog/2014/11/compiler.html
  5. https://blog.csdn.net/zhengqijun_/article/details/51881149
  6. https://www.cnblogs.com/Goldworm/archive/2012/05/21/2511910.html
  7. https://juejin.im/entry/5c0d23b35188253b7e7480db
  8. https://www.zhihu.com/question/280665935
  9. http://www.shanghai.ws/gnu/gcc_1.htm
  10. https://wiki.ubuntu.org.cn/Compiling_C
  11. https://www.cnblogs.com/sunsky303/p/7731911.html
如果有收获,可以请我喝杯咖啡!