GCC编译器背后的故事

一、用 gcc 生成 .a 静态库和 .so 动态库

静态库:在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。

动态库:库在程 序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需 要动态库存在。

  1. 创建一个 test1 文件夹,并在该文件夹中创建三个子程序 hello.h、hello.c 和 main.c

    mkdir test1	# 创建test1文件夹
    cd test1 # 进入该文件
    vim hello.h	# 编辑hello.h
    vim hello.c	# 编辑hello.c
    vim main.c	# 编辑main.c
    

    程序 hello.h 内容如下:

    #ifndef HELLO_H 
    #define HELLO_H 
    void hello(const char *name); 
    #endif //HELLO_H
    

    程序 hello.c 内容如下:

    #include <stdio.h> 
    #include "hello.h"
    void hello(const char *name) 
    {
          
          
    	printf("Hello %s!\n", name);
    }
    

    程序 main.c 内容如下:

    #include "hello.h" 
    int main() 
    {
          
          
    	hello("everyone"); 
    	return 0; 
    }
    
  2. 将 hello.c 编译成 .o文件

    无论静态库,还是动态库,都是由.o 文件创建的,因此需先编译成 .o文件

    gcc -c hello.c	# 编译成 .o文件
    ls # 查看
    
  3. 由 .o文件创建静态库,并在程序中使用

    a. .o文件创建静态库

    静态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为 .a。如:libmyhello.a

    ar -crv libmyhello.a hello.o # 生成静态库
    ls # 查看
    

    b. 在程序中使用静态库

    # 方法一
    gcc -o hello main.c -L. -lmyhello
    # 方法二
    gcc main.c libmyhello.a -o hello
    # 方法二
    gcc -o main.c	# 先生成 main.o
    gcc -o hello main.o libmyhello.a
    

    -L.:表示要连接的库在当前目录中

    然后 ./hello,执行程序:

    我们可尝试删除 libmyhello静态库,再次执行 hello 程序(看程序运行时,是否需要该静态库)

    rm libmyhello.a	# 删除libmyhello.a
    ./hello	# 运行hello程序
    

    结果:静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。

  4. 由 .o文件创建动态库,并在程序中使用

    a. .o文件创建动态库

    动态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为 .so。如:libmyhello.so

    gcc -shared -fPIC -o libmyhello.so hello.o	# 生成动态库
    ls	# 查看
    

    -shared:该选项指定生成动态连接库

    -fPIC:表示编译为位置独立的代码

    b. 在程序中使用动态库

    # 方法一
    gcc -o hello main.c -L. -lmyhello
    # 方法二
    gcc main.c libmyhello.so -o hello
    

    但运行 hello程序时将会报错(在/usr/lib 中找不到该库文件)

    原因:程序在运行时, 会在/usr/lib 和/lib 等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。

    此时我们再次生成以下 libmyhello.a 静态库,判断静态库和动态库同名时,gcc命令会使用哪个库文件:

    ar -crv libmyhello.a hello.o # 生成静态库
    gcc -o hello main.c -L. -lmyhello
    ./hello
    

    同样也会报错,由此可知当静态库和动态库同名时,gcc 命令将优先使用动态库,默认去连/usr/lib 和/lib 等目录中的动态库

    解决方法:将文件 libmyhello.so 移动到目录/usr/lib 中

    sudo mv libmyhello.so /usr/lib
    ./hello
    

二、动态库和静态库生成可执行文件大小的对比

  1. 创建一个 test2文件夹,并在该文件夹中分别创建子程序 sub1.h、sub1.c、sub2.h、sub2.c、main.c

    mkdir test2
    cd test2
    vim sub1.h
    vim sub1.c
    vim sub2.h
    vim sub2.c
    vim main.c
    

    sub1.h 内容如下:

    #ifndef SUB1_H
    #define SUB1_H 
    float x2x(int a, int b);
    #endif //SUB1_H
    

    sub1.c 内容如下:

    #include"sub1.h"
    
    float x2x(int a, int b){
          
          
    	return a + b;	//相加
    }
    

    sub2.h 内容如下:

    #ifndef SUB2_H
    #define SUB2_H 
    float x2y(int a, int b);
    #endif //SUB2_H
    

    sub2.c 内容如下:

    #include"sub2.h"
    
    float x2y(int a, int b){
          
          
            return a * b;	//相乘
    }
    

    main.c 内容如下:

    #include<stdio.h>
    #include"sub1.h"
    #include"sub2.h"
    
    int main(){
          
          
    	int a = 2, b = 3;
    	printf("%d + %d = %f\n", a, b, x2x(a, b));
    	printf("%d × %d = %f\n", a, b, x2y(a, b));
    	return 0;
    }
    
  2. 用静态库文件进行链接,生成可执行文件

    a. 将 sub1.c、sub2.c 编译成 .o文件

    gcc -c sub1.c sub2.c
    ls
    

    b. .o文件创建静态库

    ar -crv libsub1.a sub1.o
    ar -crv libsub2.a sub2.o
    ls
    

    c. 在程序中使用静态库

    gcc main.c libsub1.a libsub2.a -o main1
    ./main1
    
  3. 用动态库文件进行链接,生成可执行文件

    a. .o文件创建动态库

    gcc -shared -fPIC -o libsub1.so sub1.o
    gcc -shared -fPIC -o libsub2.so sub2.o
    ls
    

    b. 在程序中使用动态库

    gcc main.c libsub1.so libsub2.so -o main2
    # 将文件 libsub1.so、libsub2.so 移动到目录/usr/lib 中
    sudo mv libsub1.so /usr/lib
    sudo mv libsub2.so /usr/lib
    ./main2
    

    d. 两个可执行文件大小的比较

    按上述方法由静态库链接生成的可执行文件,不是完全由静态库链接生成的,因为在 main.c 中调用的 stdio.h 是由动态链接的,所以需要重新由静态库链接生成一个可执行文件,否则可能将会出现静态库生成的可执行文件小于动态库生成的

    gcc -static main.c libsub1.a libsub2.a -o main1	# 重新由静态库生成
    size main1
    ldd main1
    size main2
    ldd main2
    

    size:用于查看文件大小

    ldd:查看链接了那些动态库

三、gcc编译器是怎么编译的

  1. 创建一个 test0 文件夹,并在该文件夹中创建一个 hello.c 程序

    mkdir test0
    cd test0
    vim hello.c
    

    hello.c 内容如下:

    #include <stdio.h>
    int main(void)
    {
          
          
    	printf("Hello World! \n");
    	return 0;
    }
    
  2. 程序的编译过程

    a. 预编译(将源文件 hello.c 文件预处理生成 hello.i)

    gcc -E hello.c -o hello.i
    

    b. 编译(将预处理生成的 hello.i 文件编译生成汇编程序 hello.s)

    gcc -S hello.i -o hello.s
    

    c. 汇编(将编译生成的 hello.s 文件汇编生成目标文件 hello.o)

    # 用gcc进行汇编
    gcc -c hello.s -o hello.o
    # 用as进行汇编
    as -c hello.s -o hello.o
    

    d. 链接(分为静态链接和动态链接,生成可执行文件)

    # 动态链接
    gcc hello.c -o hello
    # 静态链接
    gcc -static hello.c -o hello
    

    e. 用 size 查看文件大小,ldd链接了那些动态库

  3. ELF 文件的分析

    a. 一个典型的 ELF 文件包含下面几个段

    (1) .text:已编译程序的指令代码段

    (2) .rodata:ro 代表 read only,即只读数据(譬如常数 const)

    (3) .data:已初始化的 C 程序全局变量和静态局部变量

    (4) .bss:未初始化的 C 程序全局变量和静态局部变量

    (5) .debug:调试符号表,调试器用此段的信息帮助调试

    readelf -S hello  # 查看各个section(段)的信息
    

    b. 反汇编 ELF

    objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:

    gcc -o hello -g hello.c
    objdump -S hello
    

    或者直接使用 objdump -D hello进行反汇编(不会有C语言源码)

  4. 在ubuntu中下载安装nasm,对示例汇编代码“hello.asm”编译生成可执行程序,并与上述用C代码的编译生成的可执行程序大小进行对比

    a. 安装nasm编译器

    下载 NSAM 软件包:https://www.nasm.us/pub/nasm/releasebuilds/2.14rc16/nasm-2.14rc16.tar.gz

    进入下载文件夹,解压该文件:

    cd 下载
    tar zxvf nasm-2.14rc16.tar.gz
    

    安装:

    cd nasm-2.14rc16/
    ./configure
    make
    sudo make install
    

    查看是否安装成功:

    nasm -version
    

    b. 编译汇编 hello.asm文件,并于C代码的编译生成的程序大小进行对比

    hello.asm 内容如下:

    ; hello.asm 
    section .data            ; 数据段声明
            msg db "Hello, world!", 0xA     ; 要输出的字符串
            len equ $ - msg                 ; 字串长度
    section .text            ; 代码段声明
    global _start            ; 指定入口函数
    _start:                  ; 在屏幕上显示一个字符串
            mov edx, len     ; 参数三:字符串长度
            mov ecx, msg     ; 参数二:要显示的字符串
            mov ebx, 1       ; 参数一:文件描述符(stdout) 
            mov eax, 4       ; 系统调用号(sys_write) 
            int 0x80         ; 调用内核功能
                             ; 退出程序
            mov ebx, 0       ; 参数一:退出代码
            mov eax, 1       ; 系统调用号(sys_exit) 
            int 0x80         ; 调用内核功能
    
    

    编译(elf 默认为32位):

    nasm -f elf64 hello.asm
    

    链接:

     ld -s -o hello hello.o
    

    c. 汇编与C代码的编译生成的可执行程序大小对比

    由此可见,直接由汇编编译生成的可执行程序比直接由C代码编译生成的可执行要小得多

四、了解实际程序是如何借助第三方库函数完成代码设计

  1. 以游客身份体验一下即将绝迹的远古时代的 BBS (一个用键盘光标控制的终端程序)

    a. 在 win10 系统中,“控制面板”–>“程序”—>“启用或关闭Windows功能”,启用 “telnet client”“适用于Linux的Windows子系统”(后面会使用),然后重启。

    b. 打开一个 cmd命令行窗口,输入如下命令:

     telnet bbs.newsmth.net
    

    以游客身份体验一下即将绝迹的远古时代的 BBS (一个用键盘光标控制的终端程序)

  2. Linux 环境下C语言编译实现贪吃蛇游戏

    a. 了解Linux 系统中终端程序最常用的光标库(curses)

    • initscr(): initscr() 是一般 curses 程式必须先呼叫的函数, 一但这个函数被呼叫之后, 系统将根据终端机的形态并启动 curses 模式
    • endwin(): curses 通常以呼叫 endwin() 来结束程式. endwin() 可用来关闭curses 模式, 或是暂时的跳离 curses 模式
    • refresh(): refresh() 为 curses 最常呼叫的一个函式
    • move(y,x): 将游标移动至 x,y 的位置
    • echochar(ch)/addch(ch): 显示某个字元

    更多库函数的详细功能请参考:Linux curses库

    b. Ubuntu18.04 安装curses库

    sudo apt-get install libncurses5-dev
    

    可通过 whereis 命令头文件和库文件都被安装到哪些目录中:

    c. Linux 环境下C语言编译实现贪吃蛇游戏

    mkdir testSnake # 新建一个文件夹
    cd testSnake # 进入该文件
    vim mysnake.c
    gcc mysnake.c -lcurses -o mysnake # 编译链接生成可执行文件
    ./mysnake
    
    

    mysnake.c 的内容请参考: Linux 环境下C语言编译实现贪吃蛇游戏

    编译时会有个警告(可以不用管)

    -lcurses:链接curses库

    ./mysnake 运行该程序,效果如下:

五、总结

通过此次实验了解如何用 gcc 生成静态库(*.a)和动态库(*.so),并且用静/动态库链接生成可执行文件;同时一个程序的编译过程分为"预编译 —> 编译 —> 汇编 —> 链接" 四个过程。

六、参考链接

猜你喜欢

转载自blog.csdn.net/xwmrqqq/article/details/109071198