最近在看APUE,不愧是經典,看一點就收穫一點。可是感受有些東西仍是沒說清楚,須要本身動手驗證一下,結果發現須要用gcc,就瞭解一下。html
有時候,你在代碼裏面引用了一個函數可是沒有包含相關的頭文件,這個時候gcc報的錯誤比較詭異,通常是這樣:【math.c:6:25: 警告:隱式聲明與內建函數‘sin’不兼容 [默認啓用]】。這個錯誤網上大量博客都在說須要包含XXX.h文件,可是沒有人解釋這個錯誤信息爲何這樣表達。什麼是隱式聲明,什麼是內建函數,我就糾結了。linux
隱式聲明函數的概念網上有相關的資料,有興趣的同窗能夠自行查閱,這裏簡要的提一下。若是你調用了一個函數a,可是gcc找不到函數a的定義,那就默認幫你定義一個函數a,大概以下。編程
int a(XXX){return XXX}ide
顯然這個不是件好事,由於,有時候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的內置函數了。ui
上面對內置函數進行了也說明,不知道我表達清楚沒有,下面講幾個具體的例子。spa
1、不鏈接libm的狀況下使用sin函數prototype
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內建函數的一些瞭解以及一些猜想,若有說的很差的地方,同窗們見諒,若有說的不對的地方,歡迎指正。