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

最近發現之前學習的課程大多數都忘得差不多了,就撿一下比較重要的複習一下,做個筆記。

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

1.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 <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

3 參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章