最近发现之前学习的课程大多数都忘得差不多了,就捡一下比较重要的复习一下,做个笔记。
C++从源文件到最终的可执行文件经历了如上图四个过程:预编译,编译,汇编,链接。其中四个阶段分别涉及到的工具有: 预处理器(preprocessor)、 编译器(compiler)、汇编器(assembler)、 链接器(linker)。
预编译阶段使用的是预处理器。
完成的主要工作有:
#if,#ifdef,#ifndef,#endif
;#include
头文件包含问题,将包含的文件复制插入到对应的位置,该过程可以递归进行;#pragma
编译器指令。 编译期阶段使用的是编译器。
完成的主要工作有:
汇编阶段使用的是汇编器。
完成的主要工作有:
链接阶段使用的是链接器。
完成的主要工作有:
下面的演示会使用如下文件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;
}
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;
}
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
!...
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
g++ -o main main.cpp add.cpp
利用上面命令编译链接生成最终的可执行文件。
由于虚拟内存空间的存在,当可执行文件被加载产生一个进程的时候,进程本身看到只是一个逻辑的虚拟内存空间,即整个空间中只有内核和自身的存在。程序状态到内存的大概结构如上面的图所示,主要分为:保留区,进程空间,内核空间。
其中进程空间包含:
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