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

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

1. 引言

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

2. 源代码

源代码文件分为两个,hello.hhello.cppmain.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.hppstatic.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.cppstatic.cpp之后可以得到两个文件hello.ostatic.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库,staticlibstatic.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.cppdynamic.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

参考资料

  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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容