美文网首页
gcc/g++使用自定义的同名函数覆盖C库函数

gcc/g++使用自定义的同名函数覆盖C库函数

作者: 哈莉_奎茵 | 来源:发表于2018-05-15 03:01 被阅读0次

    前言

    其实这问题以前就想过,每次都没有深究到底。原因在于无论是哪本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编译过程如下

    $ gcc main.c libfoo.a
    

    这个过程中,首先编译源码main.c得到一个可重定位目标文件,其中符号表中包含未解析的符号引用foo,此时链接器记录下来,然后在后面的可重定位目标文件(静态库)中查找是否含有foo的符号定义,若找到则匹配,之后不再查找定义。
    比如现在给出另一个定义了foo函数的库libfoo2.a,源码如下,编译过程同libfoo.a

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

    现在分别按照不同的顺序进行链接,运行程序,观察结果

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

    印证了刚才的结论,不存在什么后面的覆盖了前面的行为。
    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
    使用前一种方法,就可以在不需要修改现有代码的基础上,调用自己对库函数的重写版本。

    相关文章

      网友评论

          本文标题:gcc/g++使用自定义的同名函数覆盖C库函数

          本文链接:https://www.haomeiwen.com/subject/fiisdftx.html