天天看點

淺談C/C++的編譯過程——源碼如何變成可執行檔案

相信很多人同我一樣,在剛剛接觸C語言的時候,隻是找了一本教材,或者是找了一套教學視訊,跟着慢慢學習C語言的文法,并沒有去多想一個.c檔案在背景究竟是經過了怎樣的步驟才最終變成.exe檔案;就在前幾天,本人閑着無聊翻開了在書架上吃灰将近一年的“全新”CSAPP,在看到其第一章的内容之後,恍然大悟,姑且水一篇部落格紀念一下。

首先我們來簡要看一下CSAPP原書上的内容:(這是我按照自己的了解結合原書内容敲下的,也許會存在事實上的錯誤,尊重原書的說法。)

一個.c源碼最終變成可執行檔案要經過以下步驟:
  • 預處理階段:預處理器根據以#開頭的指令,修改源碼内容,比如如果源碼中有 #include<stdio.h> 則預處理器會讀取檔案 stdio.h 檔案中的内容,并将其直接插入到原來的源碼檔案中,通常另存為以 .i 為擴充名的檔案。
  • 編譯階段:編譯器讀取 .i 檔案中的内容,并将其翻譯為以 .s 為擴充名的彙編語言檔案。
  • 彙編階段:彙編器将 .s 檔案翻譯成機器碼,并儲存為 .o為擴充名的檔案。
  • 連結階段:連結器将不同的 .o 檔案合并到一起,組成最終的可執行檔案;比如我們的程式裡調用了 printf 函數,作為一個C标準函數,printf 單獨存在于一個 printf.o 的檔案中,那麼連結器将會找到這個 printf.o 檔案,将其中的内容合并到我們自己的 .o 檔案中,生成可以被加載到記憶體中執行的檔案。

 光看理論沒啥意思,讓我們來手動操作一下:

首先建立一個 hello.c 檔案,在裡面敲上經典的C語言hello world代碼,儲存,進入cmd或powershell,移動到 hello.c 所在的檔案夾(linux怎麼做肯定不用我講了,逃ε=ε=ε=┏(゜ロ゜;)┛)。

之後輸入如下指令:

gcc -E hello.c -o hello.i
gcc -S hello.c -o hello.s
           

然後,打開 hello.i 我們就可以看到,預處理器已經對源碼進行了修改(檔案内容很多,我姑且放一張截圖上來)

淺談C/C++的編譯過程——源碼如何變成可執行檔案

當然了,在檔案的末尾是我們可憐的main函數:

淺談C/C++的編譯過程——源碼如何變成可執行檔案

如果想知道完整的檔案裡有什麼,當然是自己動手試一下啦(✿◡‿◡)

然後,我們再打開 hello.s 檔案,可以看到原始的 .c 被翻譯為 .s 之後的結果:

(CSDN好像沒有專門的彙編的渲染,姑且設定成C++)

.file	"hello.c"
	.text
	.def	__main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
.LC0:
	.ascii "hello world\0"
	.text
	.globl	main
	.def	main;	.scl	2;	.type	32;	.endef
	.seh_proc	main
main:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$32, %rsp
	.seh_stackalloc	32
	.seh_endprologue
	call	__main
	leaq	.LC0(%rip), %rcx
	call	puts
	movl	$0, %eax
	addq	$32, %rsp
	popq	%rbp
	ret
	.seh_endproc
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	puts;	.scl	2;	.type	32;	.endef
           

接下來我們還可以用:

gcc hello.c -o hello.o
           

生成 .o 檔案,不過到了這一步。這些東西就完全超出我的姿勢範圍了。

單個檔案玩過了,當然還要玩一下多檔案才過瘾,讓我們準備三個檔案: hell2.c func.c func.h ,内容依次如下:

// hello2.c
#include <stdio.h>
#include "func.h"

int main()
{
	say_hello();
}
           
// func.h
#ifndef FUNC_H
#define FUNC_H

#include <stdio.h>
void say_hello();

#endif
           
// func.c
#include "func.h"

void say_hello()
{
	printf("hello\n");
}
           

之後還是進入指令行模式,依次輸入以下指令:

gcc -c func.c -o func.o
gcc -E hello2.c -o hello2.i
gcc -S hello2.c -o hello2.s
gcc hello2.c func.o -o hello2.exe
           

如果沒有錯誤,我們将得到 func.c 編譯生成的 .o 檔案, hello2.c 經過預處理器和編譯器處理之後得到的 .i 和 .s 檔案,以及 hello2.c 最終生成的可執行檔案。

打開 hello2.i ,我們将看到 #include "func.h" 被預處理器替換之後的情況:

// 上面省略100多行天書
# 1 "C:/mingw64/x86_64-w64-mingw32/include/_mingw_print_pop.h" 1 3
# 1400 "C:/mingw64/x86_64-w64-mingw32/include/stdio.h" 2 3
# 3 "hello2.c" 2
# 1 "func.h" 1






# 6 "func.h"
void say_hello();
# 4 "hello2.c" 2

int main()
{
 say_hello();
}
           

打開 hello2.s ,我們可以看到“精簡”的彙編代碼:

.file	"hello2.c"
	.text
	.def	__main;	.scl	2;	.type	32;	.endef
	.globl	main
	.def	main;	.scl	2;	.type	32;	.endef
	.seh_proc	main
main:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$32, %rsp
	.seh_stackalloc	32
	.seh_endprologue
	call	__main
	call	say_hello
	movl	$0, %eax
	addq	$32, %rsp
	popq	%rbp
	ret
	.seh_endproc
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	say_hello;	.scl	2;	.type	32;	.endef
           

這篇部落格到這裡就差不多了,水準有限,歡迎指正!

= ̄ω ̄=

繼續閱讀