最近發現之前學習的課程大多數都忘得差不多了,就撿一下比較重要的複習一下,做個筆記。
1 C++如何生成可執行文件
1.1 編譯的四個階段
C++從源文件到最終的可執行文件經歷瞭如上圖四個過程:預編譯,編譯,彙編,鏈接。其中四個階段分別涉及到的工具有: 預處理器(preprocessor)、 編譯器(compiler)、彙編器(assembler)、 鏈接器(linker)。
1.1.1 預編譯
預編譯階段使用的是預處理器。
完成的主要工作有:
- 展開所有的宏定義;
- 處理所有的條件預編譯,比如
#if,#ifdef,#ifndef,#endif
; - 處理
#include
頭文件包含問題,將包含的文件複製插入到對應的位置,該過程可以遞歸進行; - 刪除所有的註釋;
- 添加行號和文件標示,用於顯示調試信息;
- 保留
#pragma
編譯器指令。
1.1.2 編譯
編譯期階段使用的是編譯器。
完成的主要工作有:
- 詞法分析;
- 語法分析;
- 語義分析;
- 生成中間代碼;
- 代碼優化。
1.1.3 彙編
彙編階段使用的是彙編器。
完成的主要工作有:
- 將中間代碼編譯成機器碼,但是還不可執行還未進行地址映射之類的操作;
- 生成符號表;
- 生成各個段,比如數據段,代碼段等。
1.1.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 <iostream>
#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 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 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++可執行文件結構
由於虛擬內存空間的存在,當可執行文件被加載產生一個進程的時候,進程本身看到只是一個邏輯的虛擬內存空間,即整個空間中只有內核和自身的存在。程序狀態到內存的大概結構如上面的圖所示,主要分爲:保留區,進程空間,內核空間。
其中進程空間包含:
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