gcc/g++使用自定義的同名函數覆蓋C庫函數

gcc/g++使用自定義的同名函數覆蓋C庫函數

轉載參考至:https://www.jianshu.com/p/eeb...linux

前言

其實這問題之前就想過,每次都沒有深究到底。緣由在於不管是哪本Linux C編程的書,基本都會使用可靠語義的signal函數來覆蓋相應的庫函數。
好比在《Unix網絡編程》中是以下定義的:對被SIGALRM之外的信號中斷的系統調用自動重啓,而且不阻塞其餘的信號。(雖然信號掩碼是空,可是POSIX保證被捕獲的信號在其信號處理函數運行期間老是阻塞的)可是書中並未說起具體怎麼覆蓋庫函數的定義, 畢竟對於不一樣的編譯器來講作法不一樣,這裏僅針對gcc而言。編程

靜態連接VS動態連接

注:想直接看結論能夠忽略本部分的內容。

簡單來講,連接即把可重定位目標文件組合成最終的可執行目標文件(下文均以「程序」一詞代替)。而可重定向目標文件中有一個符號表,其中有一些未被解析的符號引用,好比源文件中聲明瞭一個函數,但未給出其具體定義。
這時連接器就會在其餘目標文件中查找是否有對應的符號定義。網絡

好比有下列源文件函數

// main.c
void foo();
int main() {
    foo();
    return 0;
}

能夠看到main.c中只包含foo的聲明,而沒有定義,所以直接編譯main.c會報錯。若是提供一個foo.c編譯而成的靜態庫libfoo.a(編譯過程以下)優化

// foo.c
#include <stdio.h>
void foo() { puts("foo"); }
$ gcc -c foo.c 
$ ar -rcs libfoo.a foo.o

那麼就能夠進行連接了,gcc編譯過程以下ui

$ gcc main.c libfoo.a

這個過程當中,首先編譯源碼main.c獲得一個可重定位目標文件,其中符號表中包含未解析的符號引用foo,此時連接器記錄下來,而後在後面的可重定位目標文件(靜態庫)中查找是否含有foo的符號定義,若找到則匹配,以後再也不查找定義。3d

好比如今給出另外一個定義了foo函數的庫libfoo2.a,源碼以下,編譯過程同libfoo.acode

// foo2.c
#include <stdio.h>
void foo() { puts("foo2"); }

如今分別按照不一樣的順序進行連接,運行程序,觀察結果orm

$ gcc main.c libfoo.a libfoo2.a 
$ ./a.out 
foo
$ gcc main.c libfoo2.a libfoo.a 
$ ./a.out 
foo2

印證了剛纔的結論,不存在什麼後面的覆蓋了前面的行爲。get

OK,那麼問題來了,stdio.h中只有puts函數的聲明,卻沒有定義。這就是動態庫了,能夠用ldd命令查看程序調用的動態庫

$ 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標準庫(動態庫),放在特定目錄下,而後經過gcc的-l選項指定連接的動態庫,符號定義的具體內容不會放入最終的程序中,而是記錄符號定義所在動態庫路徑,在程序運行時進行查找。優勢是簡化了程序體積,缺點是第一次調用動態連接的函數時會比較費時。

連接時,C標準庫不須要額外選項就能夠進行動態連接,只有特意加上-static選項時纔不進行動態連接,而是去靜態連接C標準庫的靜態庫。

更多細節部分能夠參考《深刻理解計算機系統》(即CSAPP)第七章

庫函數通常是進行動態連接

如何覆蓋庫函數

使用gcc選項no-builtin,在gcc的manpage中能夠看到相關說明(這裏不貼出來了),大體就是gcc對於某些內置函數會有底層優化,比本身實現一樣的功能,能產生體積更小,速度更快的底層代碼。開啓這個選項,則默認不使用系統的優化函數,而使用自定義的函數。

好比咱們來自定義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;
}

觀察不一樣編譯方式下的結果

$ gcc -c printf.c 
$ gcc main.c printf.o -fno-builtin
$ ./a.out 
my printf
hello
$ gcc main.c printf.o
$ ./a.out 
hello

對於像signal這樣的未給予優化的函數(畢竟僅僅是系統調用的包裝),直接靜態連接便可。

// signal.c
#include <stdio.h>
#include <signal.h>  // 假設signal函數的定義調用了sigaction等函數

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

另外,還可使用宏定義的方式來替換庫函數,好比

#define printf my_printf
int my_printf(const char* format, ...)
{
    // 具體實現
}

但不推薦這種作法,由於宏替換是在編譯以前進行的,最終程序中的符號信息並非printf而是my_printf,並且stdio.h中對printf的聲明也失去了意義,由於實際調用的是my_printf

使用前一種方法,就能夠在不須要修改現有代碼的基礎上,調用本身對庫函數的重寫版本。

相關文章
相關標籤/搜索