C语言编译过程解析 && 动态(.so)\静态(.a)链接区别


最近在工作中遇到编译so文件进行硬件调试的情况,遂对so文件产生了兴趣,特用这个周末研究先so文件的前世今生,以求从根源上理解它。


参考文章:

[1] 《嵌入式Linux开发教程(上册)》

[2] Linux静态库.a与动态库.so的生成与区别、以及.so库文件的封装与使用


1、首先准备三个子程序

a.c

#include <stdio.h>

/************** a.c *****************/
int printa(void)
{
    printf("this func a!\n");
    return 0;
}

a.h

#ifndef _A_H
#define _A_H

int printa(void);

#endif

b.c

#include <stdio.h>

/************** b.c *****************/
int printb(void)
{
    printf("this func b!\n");
    return 0;
}

b.h

#ifndef _B_H
#define _B_H

int printb(void);

#endif

test.c

#include "a.h"
#include "b.h"

int main(void)
{
    printa();
    printb();
    return 0;
}

2、编译过程

ztaotao@ubuntu:~/work/test1$ gcc a.c b.c test.c -o test
ztaotao@ubuntu:~/work/test1$ ls
a.c  a.h  b.c  b.h  test  test.c
ztaotao@ubuntu:~/work/test1$ ./test
this func a!
this func b!
ztaotao@ubuntu:~/work/test1$

gcc编译过程,对于test.c到test(a.out)文件,经历了test.i(预处理),test.s(汇编),test.o(),最后才得到test(a.out)文件,分别经历了预处理编译汇编链接4个步骤。

  1. 预处理:( gcc -E xxx.c -o xxx.i ) C编译器对各种预处理命令进行处理,包括头文件、宏定义的扩展、条件编译的选择等。比如对于a.c经过预处理以后,其文件内内容变为:
    ztaotao@ubuntu:~/work/test1$ gcc -E a.c -o a.i
    ztaotao@ubuntu:~/work/test1$ ls
    a.c  a.h  a.i  b.c  b.h  test  test.c
    ztaotao@ubuntu:~/work/test1$
    

    其中a.i就是预处理以后的生成文件,其中的文件内容已经由之前的几行变成了856行

    821 extern int fileno_unlocked (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    822 # 872 "/usr/include/stdio.h" 3 4
    823 extern FILE *popen (const char *__command, const char *__modes) ;
    824
    825
    826
    827
    828
    829 extern int pclose (FILE *__stream);
    830
    831
    832
    833
    834
    835 extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
    836 # 912 "/usr/include/stdio.h" 3 4
    837 extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
    838
    839
    840
    841 extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
    842
    843
    844 extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
    845 # 942 "/usr/include/stdio.h" 3 4
    846
    847 # 2 "a.c" 2
    848
    849
    850
    851 # 4 "a.c"
    852 int printa(void)
    853 {
    854     printf("this func a!\n");
    855     return 0;
    856 }
    

    分别对三个文件进行预处理

    ztaotao@ubuntu:~/work/test1$ gcc -E test.c -o test.i
    ztaotao@ubuntu:~/work/test1$ ls
    a.c  a.h  a.i  b.c  b.h  b.i  test  test.c  test.i
    ztaotao@ubuntu:~/work/test1$
    

     

  2. 编译:( gcc -S xxx.i )  将预处理得到的源代码文件,进行“翻译转换”,产生出机器语言的目标程序,得到机器语言的汇编文件

    ztaotao@ubuntu:~/work/test1$ gcc -S a.i b.i test.i
    ztaotao@ubuntu:~/work/test1$ ls *.s
    a.s  b.s  test.s
    ztaotao@ubuntu:~/work/test1$
    

    查看a.s文件内容,为汇编代码

      1     .file   "a.c"
      2     .section    .rodata
      3 .LC0:
      4     .string "this func a!"
      5     .text
      6     .globl  printa
      7     .type   printa, @function
      8 printa:
      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     movl    $.LC0, %edi
     17     call    puts
     18     movl    $0, %eax
     19     popq    %rbp
     20     .cfi_def_cfa 7, 8
     21     ret
     22     .cfi_endproc
     23 .LFE0:
     24     .size   printa, .-printa
     25     .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609"
     26     .section    .note.GNU-stack,"",@progbits
    

     

  3. 汇编 gcc -c xxx.s )将汇编代码翻译成机器代码,但还是不可以运行

    ztaotao@ubuntu:~/work/test1$ gcc -c a.s b.s test.s
    ztaotao@ubuntu:~/work/test1$ ls *.o
    a.o  b.o  test.o
    ztaotao@ubuntu:~/work/test1$
    

    此时XXX.o文件的内容已经无法阅读了,都是些机器码

  4. 链接:( gcc xxx.o )处理可重定位的文件,把各种符号引用和符号定义转换成为可执行文件中的合适信息,通常是虚拟地址。

    ztaotao@ubuntu:~/work/test1$ gcc a.o b.o test.o
    ztaotao@ubuntu:~/work/test1$ ls
    a.c  a.h  a.i  a.o  a.out  a.s  b.c  b.h  b.i  b.o  b.s  test  test.c  test.i  test.o  test.s
    ztaotao@ubuntu:~/work/test1$ ./a.out
    this func a!
    this func b!
    ztaotao@ubuntu:~/work/test1$
    

    生成了a.out可执行文件加上 -o 可以生成指定名字的可执行文件。


3、静态链接与动态链接

静态链接文件(.a文件)、动态链接文件(.so文件)

两者之间的区别:

① .a文件 :静态库文件,静态库在编译时已经被链接到目标代码中,运行程序不依赖该静态库文件
优点:将程序使用的函数的机器码复制到最终的可执行文件中,提高了运行速度;如果库函数改变,整个程序需要重新编译
缺点:所有需用到静态库的程序都会被添加静态库的那部分内容,使得可执行代码量相对变多,占用内存和硬盘空间
.so文件:动态库文件,在程序运行的时候载入动态库文件,程序要正常运行必须依赖于它需要使用的动态库文件
优点:只有在程序执行的时候, 那些需要使用的函数代码才被拷贝到内存中。动态库在内存中可以被被多个程序使用,又称之为共享库,节约了内存和磁盘空间,以时间效率去换取空间效率;当调用接口没改变,库文件发生改变重新编译,而调用的主程序可不用重新编译;
缺点:运行速度不及静态库文件;
静态库与动态库的选取使用,请结合自己的场景进行取舍,我不知道客户要使用的频率,我选择使用动态库的形式;

(1)静态链接

将a.o、b.o生成静态链接库文件

ar rc libtest.a a.o b.o//把.o文件打包成.a的库文件
gcc test.c -L. -ltest -o test_a//链接生成可执行文件
./test_a//运行测试程序
rm libtest.a //删除静态库文件
./test_a//同样正常运行程序
ztaotao@ubuntu:~/work/test1$ ar rc libtest.a a.o b.o
ztaotao@ubuntu:~/work/test1$ gcc test.c -L. -ltest -o test_a
ztaotao@ubuntu:~/work/test1$ ls
a.c  a.h  a.i  a.o  a.out  a.s  b.c  b.h  b.i  b.o  b.s  libtest  libtest.a  test  test_a  test.c  test.i  test.o  test.s
ztaotao@ubuntu:~/work/test1$ cp test_a ../
ztaotao@ubuntu:~/work/test1$ ../test_a
this func a!
this func b!
ztaotao@ubuntu:~/work/test1$ ls -l
total 132
-rw-rw-r-- 1 ztaotao ztaotao   124 Jul 13 15:29 a.c
-rw-rw-r-- 1 ztaotao ztaotao    53 Jul 13 15:31 a.h
-rw-rw-r-- 1 ztaotao ztaotao 17100 Jul 13 15:39 a.i
-rw-rw-r-- 1 ztaotao ztaotao  1504 Jul 13 15:53 a.o
-rwxrwxr-x 1 ztaotao ztaotao  8720 Jul 13 15:54 a.out
-rw-rw-r-- 1 ztaotao ztaotao   463 Jul 13 15:48 a.s
-rw-rw-r-- 1 ztaotao ztaotao   124 Jul 13 15:30 b.c
-rw-rw-r-- 1 ztaotao ztaotao    53 Jul 13 15:30 b.h
-rw-rw-r-- 1 ztaotao ztaotao 17100 Jul 13 15:41 b.i
-rw-rw-r-- 1 ztaotao ztaotao  1504 Jul 13 15:53 b.o
-rw-rw-r-- 1 ztaotao ztaotao   463 Jul 13 15:48 b.s
-rw-rw-r-- 1 ztaotao ztaotao  3222 Jul 13 16:05 libtest
-rw-rw-r-- 1 ztaotao ztaotao  3222 Jul 13 16:10 libtest.a
-rwxrwxr-x 1 ztaotao ztaotao  8720 Jul 13 15:35 test
-rwxrwxr-x 1 ztaotao ztaotao  8720 Jul 13 16:11 test_a
-rw-rw-r-- 1 ztaotao ztaotao   131 Jul 13 15:33 test.c
-rw-rw-r-- 1 ztaotao ztaotao   285 Jul 13 15:41 test.i
-rw-rw-r-- 1 ztaotao ztaotao  1432 Jul 13 15:53 test.o
-rw-rw-r-- 1 ztaotao ztaotao   405 Jul 13 15:48 test.s
ztaotao@ubuntu:~/work/test1$

告诉你这个静态库有两个接口,分别是

int printa(void); /* 打印 funa */
int printb(void); /* 打印 funb */

b用户拿到这个静态库进行开发:

test_b.c

#include <stdio.h>

int main(void)
{
    printa();
    printb();
    return 0;
}

进行编译(静态链接)

gcc test_b.c -L. ltest test_b

生成执行文件,并成功执行

ztaotao@ubuntu:~/b$ gcc test_b.c -L. -ltest -o test_b
test_b.c: In function ‘main’:
test_b.c:5:5: warning: implicit declaration of function ‘printa’ [-Wimplicit-function-declaration]
     printa();
     ^
test_b.c:6:5: warning: implicit declaration of function ‘printb’ [-Wimplicit-function-declaration]
     printb();
     ^
ztaotao@ubuntu:~/b$ ls
libtest.a  test_b  test_b.c
ztaotao@ubuntu:~/b$ ./test_b
this func a!
this func b!
ztaotao@ubuntu:~/b$
ztaotao@ubuntu:~/b$ ls -l
total 20
-rw-rw-r-- 1 ztaotao ztaotao 3222 Jul 13 16:22 libtest.a
-rwxrwxr-x 1 ztaotao ztaotao 8720 Jul 13 16:22 test_b
-rw-rw-r-- 1 ztaotao ztaotao   81 Jul 13 16:21 test_b.c
ztaotao@ubuntu:~/b$ rm libtest.a   /* 删除静态库以后依然可以正常执行 */
ztaotao@ubuntu:~/b$ ./test_b
this func a!
this func b!
ztaotao@ubuntu:~/b$

对于自己写的静态库编译时发生警告,可以通过对接口函数提前声明来消除:

修改test_b.c文件的内容:

#include <stdio.h>
/************ 申明来自静态库的接口 *************/
extern int printa(void);
extern int printb(void);
/*********************************************/
int main(void)
{
    printa();
    printb();
    return 0;
}
ztaotao@ubuntu:~/b$ cp ../work/test1/libtest.a .
ztaotao@ubuntu:~/b$ gcc test_b.c -L. -ltest -o test_b
ztaotao@ubuntu:~/b$ ./test_b
this func a!
this func b!
ztaotao@ubuntu:~/b$

(2)动态链接

对于驱动开发来说,用到so的频率要远高于静态库,因为短时间进行代码驱动调试是很有必要的。

所以,常常将修改后的代码编译成so文件,然后供原先的代码来调用。

Linux内核有很多动态链接,这意味着如果你想对某个函数进行调试,只要把这个tree下的文件重新生成一个动态so,然后Linux就会调用新的接口函数,以此达到快速调试的作用。

[3] linux下使用gcc生成动态so,并调用

编译动态链接库.so:

gcc xxx.c xxx.c -fPIC -shared -o xxx.so
ztaotao@ubuntu:~/work/test1$ gcc a.c b.c -fPIC -shared -o libtest.so
ztaotao@ubuntu:~/work/test1$ cp libtest.so ../../b/
ztaotao@ubuntu:~/work/test1$ cd ../../b/
ztaotao@ubuntu:~/b$ ls
libtest.so  test_b  test_b.c

使用.so文件:

test_b.c

#include <stdio.h>

int main(void)
{
    printa();
    printb();
    return 0;
}

动态链接:gcc xxx.c -L. -ltest -o xxx

gcc test_b -L. -ltest -o test_b



ztaotao@ubuntu:~/work/test1$ gcc test_b.c -L. -ltest -o test_b
ztaotao@ubuntu:~/b$ ls
libtest.so  test_b  test_b.c
ztaotao@ubuntu:~/b$ ./test_b
./test_b: error while loading shared libraries: libtest.so: cannot open shared object file: No such file or directory

测试是否动态链接:ldd test.b

ztaotao@ubuntu:~/b$ ldd test_b
        linux-vdso.so.1 =>  (0x00007ffc4aff0000)
        libtest.so => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d0a4d7000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0d0a8a1000)

 

注意:在动态库的使用时,由于其特殊性,编译器会到指定的目录去寻找动态库,所以你需要告诉编译器你库存放的路径,否则编译会报找不到库的错误,原因在于系统默认加载的动态链接库路径里没有找到你的动态库。

解决办法:将libtest.so放到usr/lib目录下,同时打开lib的用户访问权限:sudo chomd 755 lib

再执行./test_b,成功执行

ztaotao@ubuntu:~/b$ sudo cp libtest.so /usr/lib
ztaotao@ubuntu:~/b$ cd /
ztaotao@ubuntu:/$ cd /usr/
ztaotao@ubuntu:/usr$ ls
bin  games  include  lib  lib32  libx86_64-linux-gnu  local  locale  sbin  share  src
ztaotao@ubuntu:/usr$ sudo chmod 755 lib
ztaotao@ubuntu:/usr$ cd ~/b/
ztaotao@ubuntu:~/b$ ./test_b
this func a!
this func b!

查看链接状态:链接成功

ztaotao@ubuntu:~/b$ ldd test_b
        linux-vdso.so.1 =>  (0x00007ffef59bd000)
        libtest.so => /usr/lib/libtest.so (0x00007f9017f7a000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9017bb0000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f901817c000)
ztaotao@ubuntu:~/b$

如果改变链接库文件,也就是修改了a.c中的函数打印内容:

a.c

#include <stdio.h>

/************** a.c *****************/
int printa(void)
{
    printf("this func a!\n");
    printf("so file get!\n");
    printf("====================\n");
    return 0;
}

再次编so,不需要再次链接test_b.c,直接执行./test_b会是什么结果呢?

ztaotao@ubuntu:~/work/test1$ vi a.c
ztaotao@ubuntu:~/work/test1$ gcc a.c b.c -fPIC -shared -o libtest.so
ztaotao@ubuntu:~/work/test1$ sudo cp libtest.so /usr/lib
ztaotao@ubuntu:~/work/test1$ ./../../b/test_b
this func a!
so file get!
==============!
this func b!
ztaotao@ubuntu:~/work/test1$

可以看到./test_b的输出内容直接就变成了:

this func a!
so file get!
==============!
this func b!

在不需要再次编译的情况下就完成了代码的测试,足以看出动态链接对于测试驱动时候的优越性。


 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值