C++源文件到可执行程序
文章作者:Tyan
博客:noahsnail.com | CSDN | 简书
1. 引言
C++程序从源代码到可执行程序是一个复杂的过程,其流程为:源代码 --> 预处理 --> 编译 --> 优化 --> 汇编 --> 链接 --> 可执行文件
,本文以一段C++代码为例,按执行顺序来描述这个过程。
2. 源代码
源代码文件分为两个,hello.h
、hello.cpp
和main.cpp
,代码如下:
- hello.hpp
#ifndef HELLO_HPP_
#define HELLO_HPP_
void hello();
#endif
- hello.cpp
#include "hello.hpp"
#include <iostream>
using namespace std;
void hello() {
cout << "Hello, world!" << endl;
}
- main.cpp
#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
的命令如下:
[root@localhost:/workspace] $: g++ -E main.cpp -o main.ii
-E
参数表示预处理后即停止,不进行编译,预处理后的代码送往标准输出,-o
指定输出文件。输出文件main.ii
的内容如下:
# 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
的命令如下:
[root@localhost:/workspace] $: g++ -S main.ii
-S
参数表示编译后即停止,不进行汇编。对于每个输入的非汇编语言文件,输出文件是汇编语言文件。输出文件main.s
的内容如下:
.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
。各级优化后的结果如下:
# 默认优化,-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
的命令如下:
[root@localhost:/workspace] $: g++ -c main.s
-c
参数表示编译或汇编源文件,但是不作连接,编译器输出对应于源文件的目标文件。输出文件为main.o
,使用nm -C main.o
命令来查看文件内容,文件内容如下:
0000000000000000 T main
U hello()
7. 链接
链接是将目标文件、启动代码、库文件链接成可执行文件的过程,得到的文件可以直接执行。经过汇编之后生成的目标文件main.o
是不可以直接执行的。链接命令如下:
[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
,链接两个文件的命令如下:
[root@localhost:/workspace] $: g++ main.o hello.o -o main
经过链接,多个文件被链接成了单一的可执行文件main
,执行main
程序:
[root@localhost:/workspace] $: ./main
Hello, world!
7.1 静态链接库
除了直接链接多个目标文件之外,还可以通过链接静态库生成可执行文件。静态链接库是编译器生成的一系列对象文件的集合,库中的成员包括普通函数,类定义,类的对象实例等。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。可执行文件生成之后,就不再需要静态链接库,即编译后的可执行程序不需要外部函数库的支持。但如果静态链接库发生改变,则可执行程序需要重新编译。静态链接库属于编译时链接。
我们再添加两个static.hpp
,static.cpp
,并修改main.cpp
,内容如下:
-
static.hpp
文件:
#ifndef STATIC_HPP_
#define STATIC_HPP_
void test();
#endif
-
static.cpp
文件:
#include "static.hpp"
#include <iostream>
using namespace std;
void test() {
cout << "static lib" << endl;
}
-
main.cpp
文件:
extern void hello();
extern void test();
int main(int argc, char *argv[]) {
hello();
test();
return 0;
}
编译汇编hello.cpp
、static.cpp
之后可以得到两个文件hello.o
和static.o
,linux系统中的命令ar
,可以将多个目标文件打包成为一个单独的文件,这个文件被称为静态库。生成静态库的命令如下:
[root@localhost:/workspace] $: ar -r libstatic.a hello.o static.o
ar: creating libstatic.a
查看libstatic.a
的内容:
[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
并执行:
[root@localhost:/workspace] $: g++ main.o libstatic.a -o main
[root@localhost:/workspace] $: ./main
Hello, world!
static lib
另一种命令方式:
[root@localhost:/workspace] $: g++ -L ./ main.cpp -lstatic -o main
Linux静态库的命名惯例是名字以三个字母lib
开头并以後缀.a
结束。所有的系统库都采用这种命名惯例,并且它允许通过-l(ell)
选项来简写命令行中的库名。-lstatic
中的-l
是要求编译器在系统库目录下查找static
库,static
是libstatic.a
的简写。-L
参数用来指定要具体的查找目录,如果缺少这个参数,则只会在系统库目录下查找static
,会报错。错误如下:
[root@localhost:/workspace] $: g++ main.cpp -lstatic -o ltest
/usr/bin/ld: cannot find -lstatic
7.2 共享库
共享库(Windows叫动态链接库)是编译器以一种特殊的方式生成的对象文件的集合。对象文件模块中所有地址(变量引用或函数调用)都是相对而不是绝对的,这使得共享模块可以在程序的运行过程中被动态地调用和执行。共享库属于运行时链接。当使用共享库时,只要共享库的接口不变,共享库修改之后,不需要重新编译可执行程序。
创建dynamic.cpp
,内容如下:
#include <iostream>
using namespace std;
void test() {
cout << "dynamic lib" << endl;
}
编译hello.cpp
和dynamic.cpp
,-fpic
表示生成的对象模块采用浮动(可重定位)地址,pic
是位置无关代码(position independent code)的缩写。:
[root@localhost:/workspace] $: g++ -c -fpic hello.cpp static.cpp
使用-fpic
与不使用-fpic
生成的目标文件hello.o
:
# 使用-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
表示生成共享目标文件。:
[root@localhost:/workspace] $: g++ -shared hello.o dynamic.o -o libdynamic.so
编译main.cpp
并链接共享库:
[root@localhost:/workspace] $: g++ main.cpp libdynamic.so -o main
执行main
:
[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
中:
[root@localhost:/workspace] $: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
[root@localhost:/workspace] $: ./main
Hello, world!
dynamic lib
查看链接静态库和共享库生成的两个可执行main
文件:
# 共享库
[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
。
参考资料
- https://wiki.ubuntu.org.cn/Compiling_Cpp
- https://tech.meituan.com/2015/01/22/linker.html
- http://notes.maxwi.com/3416/06/05/source-to-program/
- http://www.ruanyifeng.com/blog/2014/11/compiler.html
- https://blog.csdn.net/zhengqijun_/article/details/51881149
- https://www.cnblogs.com/Goldworm/archive/2012/05/21/2511910.html
- https://juejin.im/entry/5c0d23b35188253b7e7480db
- https://www.zhihu.com/question/280665935
- http://www.shanghai.ws/gnu/gcc_1.htm
- https://wiki.ubuntu.org.cn/Compiling_C
- https://www.cnblogs.com/sunsky303/p/7731911.html