GCC——探寻编译器背后的故事

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

函数库分为静态库和动态库两种。 静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程 序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需 要动态库存在。

1、编辑生成例子程序

  • mkdir test1–创建工作目录
  • cd test1–打开test1
  • vim hello.h–编辑hello.h
  • vim hello.c–编辑hello.c
  • vim main.c–编辑main.c

在这里插入图片描述

vim是一个文本编辑器,基于vi发展的,vi太难用了。vim需要自己安装,进入vim后点"i"键进入编辑,编辑完成后按ESC退出编辑模式,输入英文冒号和wq,保存并退出。

vim编辑界面在这里插入图片描述

程序 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、编译生成.o文件

无论静态库,还是动态库,都是由.o 文件创建的。因此,我们必须将源程序 hello.c 通过 g cc 先编译成.o 文件。在系统提示符下键入以下命令得到 hello.o 文件。

#gcc -c hello.c 

然后ls查看是否生成hello.o
在这里插入图片描述

3、由.o 文件创建静态库

静态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为.a。例如:我们将 创建的静态库名为 myhello,则静态库文件名就是 libmyhello.a。在创建和使用静态库时, 需要注意这点。创建静态库用 ar 命令。

在系统提示符下键入以下命令将创建静态库文件 libmyhello.a

ar -crv libmyhello.a hello.o

ls查看

在这里插入图片描述

成功生成了静态库文件。

4、在程序中使用静态库

静态库制作完了,如何使用它内部的函数呢?只需要在使用到这些公用函数的源程序中包 含这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明静态库名,gcc 将会从 静态库中将公用函数连接到目标文件中。注意,gcc 会在静态库名前加上前缀 lib,然后追 加扩展名.a 得到的静态库文件名来查找静态库文件。

在程序 3:main.c 中,我们包含了静态库的头文件 hello.h,然后在主程序 main 中直接调用 公用函数 hello。下面先生成目标程序 hello,然后运行 hello 程序看看结果如何。

# 方法一
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

在这里插入图片描述
我们删除静态库文件试试公用函数 hello 是否真的连接到目标文件 hello 中了

rm libmyhello.a	# 删除libmyhello.a

在这里插入图片描述
运行没有问题,删除过后没有影响。

5、由.o 文件创建动态库文件

动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀 lib,但其 文件扩展名为.so。例如:我们将创建的动态库名为 myhello,则动态库文件名就是 libmyh ello.so。用 gcc 来创建动态库。

在系统提示符下键入以下命令得到动态库文件 libmyhello.so

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

ls查看
在这里插入图片描述

生成成功

6、在程序中使用动态库

在程序中使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含 这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明动态库名进行编译。我 们先运行 gcc 命令生成目标文件,再运行它看看结果。

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

在这里插入图片描述
报错了

error while loading shared libraries: libmyhello.so: cannot open shared object file: No such file or directory

为了探究静态库和动态库同名时,gcc命令会使用哪个库文件,重新按照上面的步骤生成静态库文件。
在这里插入图片描述
还是报同样的错,说明静态库和动态库同名时,gcc命令会使用动态库文件。
错误描述为找不到动态库文件 libmyhello.so,程序在运行时, 会在/usr/lib 和/lib 等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提 示类似上述错误而终止程序运行。
所以解决办法是将文件 libmyhello.so 复制到目录/usr/lib 中

sudo mv libmyhello.so /usr/lib

在这里插入图片描述
运行成功,问题解决。

二、静态库和动态库的实践

有了以上的经验,仿照进行实践

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、创建静态库并使用

  • 编译生成.o文件
gcc -c sub1.c sub2.c

ls查看
在这里插入图片描述

  • 由.o文件创建静态库
ar -crv libsub1.a sub1.o
ar -crv libsub2.a sub2.o

ls查看
在这里插入图片描述

  • 使用静态库创建可执行文件
gcc -static main.c libsub1.a libsub2.a -o main1	

ls查看
在这里插入图片描述

3、创建动态库并使用

  • .o文件创建动态库
gcc -shared -fPIC -o libsub1.so sub1.o
gcc -shared -fPIC -o libsub2.so sub2.o

ls查看
在这里插入图片描述

  • 使用动态库并移动所在目录
gcc main.c libsub1.so libsub2.so -o main2
sudo mv libsub1.so /usr/lib
sudo mv libsub2.so /usr/lib
./main2

在这里插入图片描述

4、查看两个可执行文件所需链接库并比较大小

  • 查看两可执行文件链接了 哪些库文件
ldd main1
ldd main2

在这里插入图片描述

  • 查看两个可执行文件的大小
size main1
size main2

在这里插入图片描述
可以看到,静态库创建的可执行文件要比动态库创建的可执行文件大得多

三、GCC编译的原理和流程

1、创建一个文件夹,并写一个hello.c文件

mkdir test0
cd test0
vim hello.c

在这里插入图片描述
hello.c

#include <stdio.h>
int main(void)
{
	printf("Hello World! \n");
	return 0;
}

2、编译过程

  • 预处理
  • 编译
  • 汇编
  • 链接(动态和静态)
gcc -E hello.c -o hello.i  #将源文件 hello.c 文件预处理生成 hello.i

gcc -S hello.i -o hello.s  #将预处理生成的 hello.i 文件编译生成汇编程序 hello.s

gcc -c hello.s -o hello.o  #将编译生成的 hello.s 文件汇编生成目标文件 hello.o
# 用gcc进行汇编
as -c hello.s -o hello.o
# 用as进行汇编
gcc hello.c -o hello  #分为静态链接和动态链接,生成可执行文件
# 动态链接
gcc -static hello.c -o hello
# 静态链接

在这里插入图片描述

size hello #查看可执行文件的大小
ldd hello  #查看链接了哪些库

在这里插入图片描述

3、分析ELF文件

  • ELF文件的段

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

.text:已编译程序的指令代码段。
.rodata:ro 代表 read only,即只读数据(譬如常数 const)。
.data:已初始化的 C 程序全局变量和静态局部变量。
.bss:未初始化的 C 程序全局变量和静态局部变量。
.debug:调试符号表,调试器用此段的信息帮助调试。
ELF 文件格式如下图所示:
在这里插入图片描述
可以使用 readelf -S 查看其各个 section 的信息如下

readelf -S hello  

在这里插入图片描述

  • 反汇编ELF

由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包 含的指令和数据,需要使用反汇编的方法

使用 objdump -D 对其进行反汇编如下:

objdump -D hello

在这里插入图片描述
objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:

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

在这里插入图片描述

4、示例汇编代码“hello.asm”与上述用C代码的编译生成的可执行程序大小对比

  • 安装nasm编译器

软件包下载网址为:https://www.nasm.us/pub/nasm/releasebuilds/2.14rc16/nasm-2.14rc16.tar.gz
我的Ubuntu18.04有自带的firefox浏览器,将网址粘贴进去打开即可下载。
下载完成后要进行解压,我的Ubuntu用户界面双击压缩包即可解压,如果没有,也可以进入终端,打开下载目录,输入命令即可解压

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

安装命令

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

安装显示项太多,这里不截图展示

nasm -v

即可查看版本,验证是否安装成功

在这里插入图片描述

安装成功。

  • 编译汇编文件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         ; 调用内核功能

顺序输入以下命令

mkdir test3  #创建文件夹
cd test3  #打开文件夹
vim hello.asm #编辑文件
nasm -f elf64 hello.asm #编译
ld -s -o hello hello.o  #链接
./hello #运行

在这里插入图片描述

  • 比较可执行文件的大小
    在这里插入图片描述

可以看到,汇编生成的可执行文件不能再小了。

四、每一个程序背后都站着一堆优秀的代码库,了解实际程序是如何借助第三方库函数完成代码设计。

1、 了解Linux 系统中终端程序最常用的光标库(curses)的主要函数功能

  • initscr(): initscr() 是一般 curses 程式必须先呼叫的函数, 一但这个函数被呼叫之後,系统将根据终端机的形态并启动 curses 模式.

  • endwin(): curses 通常以呼叫 endwin() 来结束程式.endwin() 可用来关闭curses 模式, 或是暂时的跳离 curses 模式. 如果您在程式中须要call shell ( 如呼叫system() 函式 ) 或是需要做
    system call, 就必须先以 endwin() 暂时跳离 curses 模式.最後再以wrefresh() doupdate()
    来重返 curses 模式.

  • cbreak() and nocbreak(): 当cbreak 模式被开启後, 除了 DELETE 或 CTRL 等仍被视为特殊控制字元外一切输入的字元将立刻被一一读取.当处nocbreak 模式时, 从键盘输入的字元将被储存在 buffer
    里直到输入 RETURN或 NEWLINE.在较旧版的 curses须呼叫 crmode(),nocrmode() 来取代
    cbreak(),nocbreak()

  • move(y,x): 将游标移动至 x,y 的位置.

  • getyx(win,y,x): 得到目前游标的位置. (请注意! 是 y,x 而不是&y,&x )

  • echochar(ch): 显示某个字元.

  • addch(ch): 显示某个字元.

  • mvaddch(y,x,ch): 在(x,y) 上显示某个字元. 相当於呼叫move(y,x);addch(ch);

详见文末链接

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

在 win10 系统中,“控制面板”–>“程序”—>“启用或关闭Windows功能”,启用 “telnet client” 和"适用于Linux的Windows子系统"(后面会使用)。 然后打开一个cmd命令行窗口,命令行输入 telnet bbs.newsmth.net
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3、在Ubuntu中用 sudo apt-get install libncurses5-dev 安装curses库

安装命令

sudo apt-get install libncurses5-dev

查看头文件(比如curses.h)和库文件都被安装到哪些目录中

whereis curses.h
whereis libcurses.*

在这里插入图片描述

4、用gcc编译生成一个终端游戏,体会curses库如何被链接和使用

这里生成贪吃蛇游戏,原文链接会附在文末

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

这里-lcurses链接了curses库

在这里插入图片描述
在这里插入图片描述

五、总结

这次工作很多步骤,但是这些步骤一步步引导我学会了生成静态库和动态库,并链接生成可执行文件。弄明白了gcc编译的四个步骤,尝试了汇编编译,弄清楚了这几种编译方式的区别和各自的特点。第一次使用函数库实现了一个游戏。感觉收获颇丰。

六、参考链接

贪吃蛇游戏
curses库

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值