Copyright (慣C) 2018 宅色夫
你想過 C 程式 int main(int argc, char *argv[])
背後的運作原理嗎?想過 C 程式既然「從 main() 開始執行」,那從哪裡獲取 argv[] 的內容呢?以及最後 main 函式 return 數值時,又做了什麼處理,才能讓作業系統得知程式執行結果呢?又,atexit() 一類的函式如何確保在 C 程式執行終止階段,得以執行註冊的函式?
要解答上述疑惑,我們就需要理解就 C Run-Time Library (執行階段程式庫)。C 語言開發後 (1973 年),貝爾實驗室的 Dennis Ritchie 和 Brian Kernighan 就用 C 重寫了絕大多數 UNIX 系統函式,並把其中最常用的部分獨立出來,逐漸演化我們熟知的 <stdio.h>
和 <stdlib.h>
等標頭檔,而 C run-time library 也是如此成形。隨著 C 語言的廣泛流通,各個 C 編譯器的生產商/個體/團體都遵循老的傳統,在不同平台上都有相對應的 Standard Library,但大部分實現都是與各個平台有關的。為了縮減不同 C 編譯器間的落差,ANSI C 詳細規定 C 語言各個要素的具體含義和編譯器實作要求。
1972 年到 1973 年間,C 語言及編譯器 就發展出來,1973 年的 UNIX 第 5 版以 C 語言重寫完成,驗證了 C 語言和編譯器的可靠度。但 C 語言一直到 1989 年才落實標準化,Dennis M. Ritchie 撰寫的 The Development of the C Language 提及標準化的緩慢過程
C Run-Time Library 內部包含初始化程式碼,還有錯誤處理機制 (例如 divide by zero 處理)。
和 C 語言之前的高階語言相比,如 COBOL, Fortran 和 PL/I 等程式語言,C 語言不僅相當低階 (披著高階語言皮的組合語言,從硬體的觀點是 WYSIWYG),而且關鍵字和 I/O 無關,換言之,C 語言程式需要透過標準函式庫的函式或者特製的替代品來進行。
INPUT
和 PRINTING
input
這個關鍵字摘錄自 DLL 和 Visual C++ 執行階段程式庫行為:
_DllMainCRTStartup
函式會執行基本工作,例如堆疊緩衝區安全性設定,C 執行階段程式庫 (CRT) 初始化及終止,而且會呼叫建構函式和解構函式_DllMainCRTStartup
也呼叫攔截函式的其他程式庫,例如 WinRT、 MFC 和 ATL 來執行他們自己的初始化及終止。而不需要這項初始化、 CRT 和其他程式庫,以及靜態變數,就會處於未初始化的狀態。VCRuntime
內部初始化和終止常式會呼叫是否以靜態方式連結的 CRT 或動態連結的 CRT DLL,會使用您的 DLL。Universal CRT (UCRT) 包含 C99 執行時期的函式與全局變數。UCRT
是 Windows component,伴隨 Windows 10 安裝。UCRT 的靜態函式庫、DLL 的 export libraryt、標頭檔是 Windows 10 SDK 的一部分。
vcruntime
包含 Visual C++ CRT 實作相關的程式碼,如例外處理、偵錯支援、執行時檢查、類型資訊、實現細節與特定擴充函式。
CRT 初始化庫處理行程啟動(CRT startup)、內部的逐執行緒的初始化、終止。
延伸閱讀:
思考以下程式的執行: (hello.c
)
當你在 GNU/Linux 下編譯和執行後 (gcc -o hello hello.c ; ./hello
),可用 echo $?
來取得程式返回值,也就是 1
,可是這個返回值總有機制來處理吧?所以你需要一套小程式來做,這就是 C runtime (簡稱 crt)。此外,還有 atexit
一類的函式,也需要 C runtime 的介入才能實現。
C 語言和一般的程式語言有個很重要的差異,就是 C 語言設計來滿足系統程式的需求,首先是作業系統核心,再來是一系列的工具程式,像是 ls, find, grep 等等,而我們如果忽略指標操作這類幾乎可以直接對應於組合語言的指令的特色,C 語言之所以需要 runtime,有以下幾個地方:
int main() { return 1; }
也就是 main()
結束後,要將 exit code 傳遞給作業系統的程式,這部份會放在 crt0
setjmp
和 longjmp
函式來存取,這需要額外的函式庫 (如 libgcc) 來協助處理 stack延伸閱讀: 你所不知道的C語言:數值系統篇
ArmHardFloatPort 指出在 gcc 針對 Arm 平台上有三種浮點數處理機制,也就是參數-mfloat-abi
的選項有:
- soft - this is pure software
- softfp - this supports a hardware FPU, but the ABI is soft compatible.
- hard - the ABI uses float or VFP registers.
int main(int argc, char *argv[])
背後的學問有些書上使用 void main()
的函式宣告,這是錯誤的。C++ 之父 Bjarne Stroustrup 在他的 C++ Style and Technique FAQ 中明確地寫著
"The definition void main( ) { /* … */ } is not and never has been C++, nor has it even been C."
C 語言規範 5.1.2.2.1 :
It shall be defined with a return type of int and with no parameters or with two parameters (argc and argv)
複習名詞:
"argument" 和 "parameter" 在中文翻譯一般寫「參數」或「引數」,常常混淆
argc
是 argument countargv
是 argument vectorThese pieces of data are the values of the arguments (often called actual arguments or actual parameters) with which the subroutine is going to be called/invoked. An ordered list of parameters is usually included in the definition of a subroutine, so that, each time the subroutine is called, its arguments for that call are evaluated, and the resulting values can be assigned to the corresponding parameters.
程式會使用函式呼叫,在上圖中高階語言直接將參數傳入即可,那麼在組合語言的時候是如何實作的呢?是透過暫存器? Stack ? memory ?順序又如何呢?所以我們必須要有明確規範。
我們看到包含 envp
的宣告:
故意改寫為以下:
使用 gdb 觀察:
這裡符合預期,但接下來:
看不懂了,要換個方式:
原來後 3 項是 envp (environment variables),在 C run-time 傳遞進來的內容和 printenv
輸出一致
假設 PID = 31114,那麼我們可觀察:
讀取 argv[0]
和 cmdline
來判斷執行的程式名稱,有個非常巧妙的應用: BusyBox - The Swiss Army Knife of Embedded Linux
延伸閱讀:
(過度精簡的) 編譯的流程還有格式
.coff 和 .elf 分別表示以下:
依據 Computer Science from the Bottom Up 來探討
crt0
crt0 (也稱為 c0
)
延伸閱讀: