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

使用前一种方法,就能够在不须要修改现有代码的基础上,调用本身对库函数的重写版本。

相关文章
相关标签/搜索