相信很多人同我一樣,在剛剛接觸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 我們就可以看到,預處理器已經對源碼進行了修改(檔案内容很多,我姑且放一張截圖上來)
當然了,在檔案的末尾是我們可憐的main函數:
如果想知道完整的檔案裡有什麼,當然是自己動手試一下啦(✿◡‿◡)
然後,我們再打開 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
這篇部落格到這裡就差不多了,水準有限,歡迎指正!
= ̄ω ̄=