最近在看APUE,不愧是經典,看一點就收獲一點。但是感覺有些東西還是沒說清楚,需要自己動手驗證一下,結果發現需要用gcc,就了解一下。
有時候,你在代碼里面引用了一個函數但是沒有包含相關的頭文件,這個時候gcc報的錯誤比較詭異,一般是這樣:【math.c:6:25: 警告:隱式聲明與內建函數‘sin’不兼容 [默認啟用]】。這個錯誤網上大量博客都在說需要包含XXX.h文件,但是沒有人解釋這個錯誤信息為什么這樣表達。什么是隱式聲明,什么是內建函數,我就糾結了。
隱式聲明函數的概念網上有相關的資料,有興趣的同學可以自行查閱,這里簡要的提一下。如果你調用了一個函數a,但是gcc找不到函數a的定義,那就默認幫你定義一個函數a,大概如下。
int a(XXX){return XXX}
顯然這個不是件好事,因為,有時候gcc這樣做會發現問題,提示這個錯誤,如果你用了這樣的語句int i = a(XX);這樣的話gcc是不會報錯的,具體的行為我也沒有深入研究。C語言后來的標准都慢慢放棄了隱式聲明函數,C++里面會直接報錯。
內建函數,講這個的資料就比較少。最后是在gcc的官方文檔里面看到了相關的介紹,我也沒有時間去細究只是看了幾段話,再結合一些帖子里面的只言片語,大概得出如下推測。。
顧名思義,內建函數就是一個系統或者工具提供的默認就能用的函數。這里面可以有兩種理解,可以是gcc支持的c語言默認讓你用這些函數,這些是gcc-c的內建函數;還有一種理解就是gcc指定的函數,gcc允許你使用這些函數。官方文檔里面說gcc的內建函數大多是為了對代碼進行優化,所以我更傾向於后一種理解。我覺得gcc的內建函數可以認為是gcc提供的一些類似預處理功能,以C函數的形式提供給編程人員使用,就是說看着是c函數,其實最后跟c語言沒關系。比如下面的例子里面會用到,如果代碼里面直接有sin(1)這樣的調用,那gcc會直接算出sin(1)的值,然后在生成代碼的時候直接使用這個值,而不會使用call sin命令調用sin函數。這就是所謂的優化(還有其他類型的優化,這個只是其中一種情況)。
官方文檔里面說gcc的內建函數主要分兩類,一類以_builtin_為前綴,一類沒有前綴。后者往往與某一個標准庫的函數相對應,如sin,printf,exit。當編譯器認為可以對相關的代碼進行優化的時候(比如上面提到的直接得出某個結果,比如忽略沒有意義的計算等等),會直接進行優化,而這些函數就相當於gcc的內置函數了。
上面對內置函數進行了也說明,不知道我表達清楚沒有,下面講幾個具體的例子。
一、不連接libm的情況下使用sin函數
file:math.c。
#include <stdio.h> #include <math.h> int main(){ //int i = 1; //printf("sin(1)=%f.\n", sin(i)); printf("sin(1)=%f.\n", sin(1)); return 0; }
這個代碼可以直接gcc math.c -o math.out。然后./math.out直接執行。
輸出結果:sin(1)=0.841471.
習慣了window編程的同學可能覺得沒什么,但是在linux編程中是有問題的。gcc中,include <math.h>這條語句只是將math.h(標准庫頭文件)文件包含進math.c(我們的例子文件)中來,但是math.h中只有sin函數的聲明,並沒有sin函數的定義。正常而言,使用了math.h中聲明的函數,就需要在編譯(准確說是連接)的時候指定實現了math.h中函數聲明的庫,這里math.h對應標准庫libm.a和libm.so。前者為靜態庫,后者為動態庫。你可以這樣理解,所有的.h文件是不需要編譯的(如果被include,直接就相當於插入到了代碼中),所有的.c文件都需要編譯。.h文件中只是定義一個函數的形式,而不管這個函數具體做什么,比如sin函數需要一個double型的參數,執行完后返回一個double型的值。對匯編和編譯原理有所了解的同學都應該懂,這樣就可以暫時的編譯一個調用了sin函數的.c文件,而不管sin函數具體怎定義了,直到生成匯編源代碼。最后編譯成匯編源代碼大概就是
push XXX //參數壓棧
call sin
mov XXX XXX 或者pop XXX //獲取返回值。
有個函數聲明,編譯器就知道參數壓棧怎么壓,同時也知道返回的時候怎么獲取返回值。
但是代碼最后還是要執行的,也就是說生成了匯編源代碼還不行,還要把匯編源代碼匯編成機器代碼。這個時候,沒有sin函數具體的代碼,編譯器沒辦法繼續將匯編源代碼匯編成機器代碼,只能停留在這里。編譯一份代碼的最后一步就是連接。連接會將所有指定的.c文件編譯的結果連接在一起。如上所述,libm.a和libm.so實現了sin,要想上面的代碼能夠運行,需要將libm.a(這里面只用到靜態鏈接庫)和math.c(示例代碼)的編譯結果連接起來。
說了半天編譯器的事,如果你聽不明白上面的內容,那估計就不用往下看了,先補充一下相關的知識再說。
總而言之,在gcc中如果代碼使用了math.h中聲明的函數,不但要在代碼里include <math.h>,還需要編譯的時候指定連接libm.a。理解了這點,就知道為什么上面的例子使用"gcc math.c -o math.out"很奇怪了。言歸正傳,為什么這個例子不需要連接libm.so。
一開始,我以為是gcc編譯器比較智能,能自動識別sin是math.h中的函數,然后自動連接libm.a。或者gcc默認就連接libm.a,但是網上並沒找到這樣的資料。直到看到一個帖子也是問類似的問題,有一個回答的人大意如下:gcc會對代碼進行優化,但是優化也是基於gcc能夠確定這個優化是沒問題的。比如把sin(1)替換為sin(1)的真實值,這個就可以,因為代碼里面使用sin(1)的目的99.9999999%是要計算sin(1)的值,而這個值是確定的,那gcc就在編譯的時候算好,運行的時候就不用再算了。為了驗證這點,可以使用gcc -S math.c -o math.s命令查看gcc將math.c編譯成的匯編源代碼(-S指定編譯行為停止在生成匯編源代碼階段)。
1 .file "math.c" 2 .section .rodata 3 .LC1: 4 .string "sin(1)=%f.\n" 5 .text 6 .globl main 7 .type main, @function 8 main: 9 .LFB0: 10 .cfi_startproc 11 pushq %rbp 12 .cfi_def_cfa_offset 16 13 .cfi_offset 6, -16 14 movq %rsp, %rbp 15 .cfi_def_cfa_register 6 16 subq $16, %rsp 17 movabsq $4605754516372524270, %rax 18 movq %rax, -8(%rbp) 19 movsd -8(%rbp), %xmm0 20 movl $.LC1, %edi 21 movl $1, %eax 22 call printf 23 movl $0, %eax 24 leave 25 .cfi_def_cfa 7, 8 26 ret 27 .cfi_endproc 28 .LFE0: 29 .size main, .-main 30 .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)" 31 .section .note.GNU-stack,"",@progbits
注意main函數的內容,里面只有一個call printf,並沒有call sin。同時注意到第17行有一個莫名其妙的數字$4605754516372524270。個人認為這個就是sin(1)的值經過莫中變化后的8進制代碼。至於經過了什么變化我也說不清楚,這個值好像也不是sin(1)浮點結果的8進制,可能經過了一些運算,或者sin(1)的結果只是這個8進制值的一部分,這個有心的同學可以研究研究。不管怎么樣,匯編代碼里面沒有call sin。說明sin(1)已經被優化了。
同樣是sin(1),在什么情況下gcc沒辦法優化呢?很簡單,int i = 1; sin(i),這樣gcc就沒法優化了。雖然也是計算sin(1),但是gcc在編譯代碼的時候只知道求sin(i),但是他不知道i值是多少。為什么不知道?這個是編譯優化的內容,有興趣的同學可以了解一下。簡單來說就是,有些變量的值在某些狀態下是可以推導的,但是目前的技術能推導的情況不多,而且需要大量的編譯處理才能推導,gcc對sin(i)這種情況大概是選擇直接不推導。
1 #include <stdio.h> 2 #include <math.h> 3 4 int main(){ 5 int i = 1; 6 printf("sin(1)=%f.\n", sin(i)); 7 printf("sin(1)=%f.\n", sin(1)); 8 return 0; 9 }
注意之前math.c的代碼,將其中的注釋去掉,就是現在math.c的代碼。這個時候"gcc math.c -o math.out"就會報錯:
/tmp/ccYkhbgg.o:在函數‘main’中:
math.c:(.text+0x15):對‘sin’未定義的引用
collect2: 錯誤:ld 返回 1
再看看匯編代碼,注意這個時候到匯編的代碼還是可以生成的,只是將匯編源程序會變成機器代碼的時候,才發現call sin的sin函數沒定義。
1 .file "math.c" 2 .section .rodata 3 .LC0: 4 .string "sin(1)=%f.\n" 5 .text 6 .globl main 7 .type main, @function 8 main: 9 .LFB0: 10 .cfi_startproc 11 pushq %rbp 12 .cfi_def_cfa_offset 16 13 .cfi_offset 6, -16 14 movq %rsp, %rbp 15 .cfi_def_cfa_register 6 16 subq $32, %rsp 17 movl $1, -4(%rbp) 18 cvtsi2sd -4(%rbp), %xmm0 19 call sin 20 movsd %xmm0, -24(%rbp) 21 movq -24(%rbp), %rax 22 movq %rax, -24(%rbp) 23 movsd -24(%rbp), %xmm0 24 movl $.LC0, %edi 25 movl $1, %eax 26 call printf 27 movabsq $4605754516372524270, %rax 28 movq %rax, -24(%rbp) 29 movsd -24(%rbp), %xmm0 30 movl $.LC0, %edi 31 movl $1, %eax 32 call printf 33 movl $0, %eax 34 leave 35 .cfi_def_cfa 7, 8 36 ret 37 .cfi_endproc 38 .LFE0: 39 .size main, .-main 40 .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)" 41 .section .note.GNU-stack,"",@progbits
這個時候有兩個call printf,第一個call printf之前有一個call sin。第二個call printf前面還是沒有call sin。
gcc官方文檔里面有一段話,大意是:對於內置函數,如果能對代碼進行優化,gcc會優化代碼,如果不能優化,往往就是直接調用同名的標准庫函數。我的理解就是sin(1)能優化就給你優化了,sin(i)優化不了,就還是調用math.h中聲明的sin函數。
GCC includes built-in versions of many of the functions in the standard C library. These functions come in two forms: one whose names start with the __builtin_
prefix, and the other without. Both forms have the same type (including prototype), the same address (when their address is taken), and the same meaning as the C library functions even if you specify the -fno-builtin option see C Dialect Options). Many of these functions are only optimized in certain cases; if they are not optimized in a particular case, a call to the library function is emitted.
修改的后代碼編譯時制定libm.a就可以,具體命令如下 gcc math.c -lm -o math.out。 -lxxx參數就是到相關目錄中找libxxx.so和libxxx.a。這樣就可以連接到libm.a了。
gcc內建函數是可選的,我們可以在編譯的時候指定不使用某些內建函數,gcc -fno-builtin-xxx。還是一開始的例子,使用命令:gcc -fno-builtin-sin math.c -o math.out。這次就會報錯,因為我們指定不使用內建函數sin,那就會使用math.h中聲明的sin函數,同時編譯的時候並沒有指定連接libm.a,這樣就會報錯:
/tmp/ccKy8vEG.o:在函數‘main’中:
math.c:(.text+0x11):對‘sin’未定義的引用
collect2: 錯誤:ld 返回 1
最初的問題,【math.c:6:25: 警告:隱式聲明與內建函數‘sin’不兼容 [默認啟用]】是什么意思?這個其實我自己也不清楚,我只是大概弄清楚了什么叫做隱式聲明函數和內建函數。在論壇上有人這樣回復:內建函數也是有原型的,當隱式聲明和對應的內建函數的聲明不一致的時候,可能會出問題,所以gcc就警告一下。
最后一個默認啟用是什么意思我就不清楚了,推測是使用內置函數。
最后補充一個例子
1 #include <stdio.h> 2 //#include <math.h> 3 4 int main(){ 5 int i = 1; 6 printf("sin(1)=%f.\n", sin(i)); 7 //printf("sin(1)=%f.\n", sin(1)); 8 return 0; 9 }
編譯的時候使用 gcc -lm math.c -o math.out。會有【math.c:6:25: 警告:隱式聲明與內建函數‘sin’不兼容 [默認啟用]】警告,但是卻還是能生成可執行文件,並且執行結果正確。這個例子中,我們沒有包含math.h,所以sin肯定是一個隱式聲明函數,會和內建函數不兼容,gcc發出警告,但是由於gcc無法優化sin(i),所以轉而調用標准庫的sin(這個調用應該是內置的,因為我們沒有包含math.h,應該gcc自動調用math.c中sin函數)。同時連接的時候制定了-lm,連接成功。所以生成的可執行文件正常計算sin(1)。如果默認啟用是使用隱式聲明函數,那結果應該會有問題。
好了,這些就是我對gcc內建函數的一些了解以及一些猜測,如有說的不好的地方,同學們見諒,如有說的不對的地方,歡迎指正。