C++如何生成可执行文件

最近发现之前学习的课程大多数都忘得差不多了,就捡一下比较重要的复习一下,做个笔记。

1 C++如何生成可执行文件

1.1 编译的四个阶段

C++如何生成可执行文件_第1张图片

  C++从源文件到最终的可执行文件经历了如上图四个过程:预编译,编译,汇编,链接。其中四个阶段分别涉及到的工具有: 预处理器(preprocessor)、 编译器(compiler)、汇编器(assembler)、 链接器(linker)。

1.1.1 预编译

  预编译阶段使用的是预处理器。
  完成的主要工作有:

  1. 展开所有的宏定义;
  2. 处理所有的条件预编译,比如#if,#ifdef,#ifndef,#endif
  3. 处理#include头文件包含问题,将包含的文件复制插入到对应的位置,该过程可以递归进行;
  4. 删除所有的注释;
  5. 添加行号和文件标示,用于显示调试信息;
  6. 保留#pragma编译器指令。

1.1.2 编译

  编译期阶段使用的是编译器。
  完成的主要工作有:

  1. 词法分析;
  2. 语法分析;
  3. 语义分析;
  4. 生成中间代码;
  5. 代码优化。

1.1.3 汇编

  汇编阶段使用的是汇编器。
  完成的主要工作有:

  1. 将中间代码编译成机器码,但是还不可执行还未进行地址映射之类的操作;
  2. 生成符号表;
  3. 生成各个段,比如数据段,代码段等。

1.1.4 链接

  链接阶段使用的是链接器。
  完成的主要工作有:

  1. 合并各个段,调整段的大小和起始位置;
  2. 符号解析;
  3. 分配地址和空间,找到符号对应的虚拟内存地址;
  4. 符号重定位。

1.2 演示

  下面的演示会使用如下文件add.hpp,add.cpp,main.cpp,三个文件的内容如下列出:

➜  workshop tree
.
├── add.cpp
├── add.hpp
└── main.cpp

0 directories, 3 files
//add.hpp
#pragma once
#ifndef __ADD_H__
#define __ADD_H__

#define ADD_NO(a, b) ((a) + (b))
#define CONST_VALUE 10

int add(int rst, int snd);

#endif
//add.cpp
#include "add.hpp"

int add(int rst, int snd)
{
    return rst + snd;
}
//main.cpp
#include 
#include "add.hpp"

using std::cout;
using std::endl;
//main file
int main()
{
    const int value = 20;
    int arr[CONST_VALUE];

    cout << "宏定义add:" << ADD_NO(1, 2) << endl;
    cout << "hello generator!" << endl;
    cout << "1 + 2 == " << add(1, 2) << endl;
    int ret = add(value, ADD_NO(value, value));
    cout << "add const:" << ret << endl;
    return 0;
}

1.2.1 预编译

gcc -E main.cpp -o main.i
gcc -E add.cpp -o add.i

  通过上面的命令可以生成main.i,add.i预编译文件,大概内容如下:

  • add.i
# 1 "add.cpp"
# 1 ""
# 1 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "" 2
# 1 "add.cpp"
# 1 "add.hpp" 1







int add(int rst, int snd);
# 2 "add.cpp" 2

int add(int rst, int snd)
{
    return rst + snd;
}
  • main.i,由于iostream的内容太多了就省略了。从下面的可以看到宏定义都被展开了,条件预编译都被替换了,注释被删除:
//这里省略了大量的iostream的内容
# 2 "main.cpp" 2
# 1 "add.hpp" 1








# 8 "add.hpp"
int add(int rst, int snd);
# 3 "main.cpp" 2

using std::cout;
using std::endl;

int main()
{
    const int value = 20;
    int arr[10];

    cout << "宏定义add:" << ((1) + (2)) << endl;
    cout << "hello generator!" << endl;
    cout << "1 + 2 == " << add(1, 2) << endl;
    int ret = add(value, ((value) + (value)));
    cout << "add const:" << ret << endl;
    return 0;
}

1.2.2 编译

gcc -S main.cpp -o main.s
gcc -S add.cpp -o add.s

  通过上述命令得到的是汇编文件,下面只贴出add.s和部分main.s

	.file	"add.cpp"
	.text
	.globl	_Z3addii
	.type	_Z3addii, @function
_Z3addii:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	-4(%rbp), %edx
	movl	-8(%rbp), %eax
	addl	%edx, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	_Z3addii, .-_Z3addii
	.ident	"GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
	.section	.note.GNU-stack,"",@progbits

  从节选的内容中可以看到常量字符串都被放到了常量区,const修饰的值都被替换成立即数,比如movl $20, -72(%rbp),函数调用都替换成函数的符号,比如call _Z3addii,即int add(int,int),此时和后面的汇编完的代码都没有准确的函数地址,需要进行链接加载。

	.file	"main.cpp"
	.local	_ZStL8__ioinit
	.comm	_ZStL8__ioinit,1,1
	.section	.rodata
.LC0:
	.string	"\345\256\217\345\256\232\344\271\211add:"
.LC1:
	.string	"hello generator!"
.LC2:
	.string	"1 + 2 == "
.LC3:
	.string	"add const:"
	.text
	.globl	main
	.type	main, @function
main:
.LFB1021:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	pushq	%rbx
	subq	$72, %rsp
	.cfi_offset 3, -24
	movq	%fs:40, %rax
	movq	%rax, -24(%rbp)
	xorl	%eax, %eax
	movl	$20, -72(%rbp)
	!...
	movl	$40, %esi
	movl	$20, %edi
	call	_Z3addii
	movl	%eax, -68(%rbp)
	movl	$.LC3, %esi
	movl	$_ZSt4cout, %edi
	call	_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    !...

1.2.3 汇编

gcc -c main.cpp -o main.o
gcc -c add.cpp -o add.o

  通过上述命令会生成机器码,但是部分函数地址之类的数据并未进行链接,只是一个符号因此无法执行。下面为main.o的部分内容,可以看到其中的字符串常量和函数符号addii依然存在,而不是准确的函数地址。

ELF>
@@UH��SH��HdH�%(H�E�1��E�����H����H�������H�������þ����H����H����(���E����H�‹E���H����H����H�M�dH3%(t�H��H[]�UH��H���}��u��}�u'�}���u���������UH���������]�宏定义add:hello generator!1 + 2 == add const:GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609zRx� �A�C
E��@>A�C
y `A�C
P��
�>I7	
X�]g�����'4Lmain.cpp_ZStL8__ioinit_Z41__static_initialization_and_destruction_0ii_GLOBAL__sub_I_main_ZSt4cout_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc_ZNSolsEi_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ZNSolsEPFRSoS_E_Z3addii__stack_chk_fail_ZNSt8ios_base4InitC1Ev__dso_handle_ZNSt8ios_base4InitD1Ev__cxa_atexit 

1.2.4 链接

g++ -o main main.cpp add.cpp

  利用上面命令编译链接生成最终的可执行文件。

2 C++可执行文件结构

C++如何生成可执行文件_第2张图片
  由于虚拟内存空间的存在,当可执行文件被加载产生一个进程的时候,进程本身看到只是一个逻辑的虚拟内存空间,即整个空间中只有内核和自身的存在。程序状态到内存的大概结构如上面的图所示,主要分为:保留区,进程空间,内核空间。
  其中进程空间包含:

  • txt段:存放可执行代码和部分只读字符串常量;
  • data段:存放已经初始化或者初始化不为0的数据,即全局变量和静态变量;
  • bss段:存放未经过初始化或者初始化为0的数据,即全局变量和静态变量;
  • heap:堆,用户自动申请的内存空间(运行时概念);
  • 共享库:进程的共享库空间;
  • stack:栈空间,用来存储局部变量,调用函数作为栈之类(运行时概念);

  顺便提一句,C++中的const和C语言中的const不同,从上面的可以看到C++中的const在编译阶段就被替换为立即数,因此对于用户操作的const作为一个局部变量仍然在栈上。但是由于在编译期就进行了替换,因此无论用户之后如何修改该值实际上都不会产生效果。(通过指针强制转换修改)。

  main可执行文件的段:

共有 31 个节头,从偏移量 0x1cf0 开始:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       0000000000000030  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002c8  000002c8
       0000000000000168  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400430  00000430
       000000000000018d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           00000000004005be  000005be
       000000000000001e  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          00000000004005e0  000005e0
       0000000000000050  0000000000000000   A       6     2     8
  [ 9] .rela.dyn         RELA             0000000000400630  00000630
       0000000000000030  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400660  00000660
       00000000000000d8  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         0000000000400738  00000738
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400760  00000760
       00000000000000a0  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000400800  00000800
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         0000000000400810  00000810
       00000000000002d2  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000400ae4  00000ae4
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400af0  00000af0
       0000000000000038  0000000000000000   A       0     0     4
  [17] .eh_frame_hdr     PROGBITS         0000000000400b28  00000b28
       000000000000004c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         0000000000400b78  00000b78
       000000000000015c  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000600df8  00000df8
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000600e08  00000e08
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000600e18  00000e18
       00000000000001e0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000060  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000601060  00001060
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000601080  00001070
       0000000000000118  0000000000000000  WA       0     0     32
  [27] .comment          PROGBITS         0000000000000000  00001070
       0000000000000035  0000000000000001  MS       0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  00001be3
       000000000000010c  0000000000000000           0     0     1
  [29] .symtab           SYMTAB           0000000000000000  000010a8
       0000000000000780  0000000000000018          30    51     8
  [30] .strtab           STRTAB           0000000000000000  00001828
       00000000000003bb  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
   text	   data	    bss	    dec	    hex	filename
   2680	    632	    280	   3592	    e08	main

3 参考

  • gcc——预处理(预编译),编译,汇编,链接
  • 才搞清楚常量的存储位置
  • Linux:预处理、编译、汇编、链接
  • 如何查看ELF文件
  • Linux可执行文件与进程的虚拟地址空间

你可能感兴趣的:(基础知识)