前言
其實(shí)這問題以前就想過,每次都沒有深究到底沙热。原因在于無論是哪本Linux C編程的書侣滩,基本都會(huì)使用可靠語義的signal函數(shù)來覆蓋相應(yīng)的庫函數(shù)。
比如在《Unix網(wǎng)絡(luò)編程》中是如下定義的:對(duì)被SIGALRM
以外的信號(hào)中斷的系統(tǒng)調(diào)用自動(dòng)重啟涡真,并且不阻塞其他的信號(hào)分俯。(雖然信號(hào)掩碼是空,但是POSIX保證被捕獲的信號(hào)在其信號(hào)處理函數(shù)運(yùn)行期間總是阻塞的)
但是書中并未提及具體怎么覆蓋庫函數(shù)的定義哆料, 畢竟對(duì)于不同的編譯器來說做法不同缸剪,這里僅針對(duì)gcc
而言。
靜態(tài)鏈接VS動(dòng)態(tài)鏈接
注:想直接看結(jié)論可以忽略本部分的內(nèi)容剧劝。
簡(jiǎn)單來說橄登,鏈接即把可重定位目標(biāo)文件組合成最終的可執(zhí)行目標(biāo)文件(下文均以“程序”一詞代替)。而可重定向目標(biāo)文件中有一個(gè)符號(hào)表讥此,其中有一些未被解析的符號(hào)引用拢锹,比如源文件中聲明了一個(gè)函數(shù),但未給出其具體定義萄喳。
這時(shí)鏈接器就會(huì)在其他目標(biāo)文件中查找是否有對(duì)應(yīng)的符號(hào)定義卒稳。
比如有下列源文件
// main.c
void foo();
int main() {
foo();
return 0;
}
可以看到main.c
中只包含foo
的聲明,而沒有定義他巨,因此直接編譯main.c會(huì)報(bào)錯(cuò)充坑。如果提供一個(gè)foo.c
編譯而成的靜態(tài)庫libfoo.a
(編譯過程如下)
// foo.c
#include <stdio.h>
void foo() { puts("foo"); }
$ gcc -c foo.c
$ ar -rcs libfoo.a foo.o
那么就可以進(jìn)行鏈接了,gcc編譯過程如下
$ gcc main.c libfoo.a
這個(gè)過程中染突,首先編譯源碼main.c
得到一個(gè)可重定位目標(biāo)文件捻爷,其中符號(hào)表中包含未解析的符號(hào)引用foo
,此時(shí)鏈接器記錄下來份企,然后在后面的可重定位目標(biāo)文件(靜態(tài)庫)中查找是否含有foo
的符號(hào)定義也榄,若找到則匹配,之后不再查找定義。
比如現(xiàn)在給出另一個(gè)定義了foo
函數(shù)的庫libfoo2.a
甜紫,源碼如下降宅,編譯過程同libfoo.a
// foo2.c
#include <stdio.h>
void foo() { puts("foo2"); }
現(xiàn)在分別按照不同的順序進(jìn)行鏈接,運(yùn)行程序囚霸,觀察結(jié)果
$ gcc main.c libfoo.a libfoo2.a
$ ./a.out
foo
$ gcc main.c libfoo2.a libfoo.a
$ ./a.out
foo2
印證了剛才的結(jié)論腰根,不存在什么后面的覆蓋了前面的行為。
OK拓型,那么問題來了额嘿,stdio.h
中只有puts
函數(shù)的聲明,卻沒有定義吨述。這就是動(dòng)態(tài)庫了岩睁,可以用ldd
命令查看程序調(diào)用的動(dòng)態(tài)庫
$ ldd a.out
linux-vdso.so.1 => (0x00007fff78b02000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe770f5a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe771324000)
libc.so.6
即C標(biāo)準(zhǔn)庫(動(dòng)態(tài)庫),放在特定目錄下揣云,然后通過gcc的-l選項(xiàng)指定鏈接的動(dòng)態(tài)庫捕儒,符號(hào)定義的具體內(nèi)容不會(huì)放入最終的程序中,而是記錄符號(hào)定義所在動(dòng)態(tài)庫路徑邓夕,在程序運(yùn)行時(shí)進(jìn)行查找刘莹。優(yōu)點(diǎn)是簡(jiǎn)化了程序體積,缺點(diǎn)是第一次調(diào)用動(dòng)態(tài)鏈接的函數(shù)時(shí)會(huì)比較費(fèi)時(shí)焚刚。
鏈接時(shí)点弯,C標(biāo)準(zhǔn)庫不需要額外選項(xiàng)就可以進(jìn)行動(dòng)態(tài)鏈接,只有特地加上-static
選項(xiàng)時(shí)才不進(jìn)行動(dòng)態(tài)鏈接矿咕,而是去靜態(tài)鏈接C標(biāo)準(zhǔn)庫的靜態(tài)庫抢肛。
更多細(xì)節(jié)部分可以參考《深入理解計(jì)算機(jī)系統(tǒng)》(即CSAPP)第七章
庫函數(shù)一般是進(jìn)行動(dòng)態(tài)鏈接
如何覆蓋庫函數(shù)
使用gcc選項(xiàng)no-builtin
,在gcc的manpage中可以看到相關(guān)說明(這里不貼出來了)碳柱,大致就是gcc對(duì)于某些內(nèi)置函數(shù)會(huì)有底層優(yōu)化捡絮,比自己實(shí)現(xiàn)同樣的功能,能產(chǎn)生體積更小莲镣,速度更快的底層代碼福稳。開啟這個(gè)選項(xiàng),則默認(rèn)不使用系統(tǒng)的優(yōu)化函數(shù)瑞侮,而使用自定義的函數(shù)的圆。
比如我們來自定義printf(只是示例,并不是還原功能)
// printf.c
#include <unistd.h>
#include <string.h>
int printf(const char* format, ...) {
write(STDOUT_FILENO, "my printf\n", 10);
write(STDOUT_FILENO, format, strlen(format));
return 0;
}
// main.c
#include <stdio.h>
int main() {
printf("hello\n");
return 0;
}
觀察不同編譯方式下的結(jié)果
$ gcc -c printf.c
$ gcc main.c printf.o -fno-builtin
$ ./a.out
my printf
hello
$ gcc main.c printf.o
$ ./a.out
hello
對(duì)于像signal
這樣的未給予優(yōu)化的函數(shù)(畢竟僅僅是系統(tǒng)調(diào)用的包裝)半火,直接靜態(tài)鏈接即可越妈。
// signal.c
#include <stdio.h>
#include <signal.h> // 假設(shè)signal函數(shù)的定義調(diào)用了sigaction等函數(shù)
typedef void Sigfunc(int);
Sigfunc* signal(int signo, Sigfunc* func) {
printf("%d\n", signo);
return func;
}
// main.c
#include <signal.h>
int main() {
signal(SIGINT, SIG_DFL);
return 0;
}
$ gcc -c signal.c
$ gcc main.c signal.o
$ ./a.out
2
另外,還可以使用宏定義的方式來替換庫函數(shù)钮糖,比如
#define printf my_printf
int my_printf(const char* format, ...)
{
// 具體實(shí)現(xiàn)
}
但不推薦這種做法梅掠,因?yàn)楹晏鎿Q是在編譯之前進(jìn)行的,最終程序中的符號(hào)信息并不是printf
而是my_printf
,而且stdio.h
中對(duì)printf
的聲明也失去了意義瓤檐,因?yàn)閷?shí)際調(diào)用的是my_printf
。
使用前一種方法娱节,就可以在不需要修改現(xiàn)有代碼的基礎(chǔ)上挠蛉,調(diào)用自己對(duì)庫函數(shù)的重寫版本。