文章目录
一、用 gcc 生成 .a 静态库和 .so 动态库
静态库:在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。
动态库:库在程 序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需 要动态库存在。
-
创建一个 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; }
-
将 hello.c 编译成 .o文件
无论静态库,还是动态库,都是由.o 文件创建的,因此需先编译成 .o文件
gcc -c hello.c # 编译成 .o文件 ls # 查看
-
由 .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程序
结果:静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。
-
由 .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
二、动态库和静态库生成可执行文件大小的对比
-
创建一个 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; }
-
用静态库文件进行链接,生成可执行文件
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
-
用动态库文件进行链接,生成可执行文件
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编译器是怎么编译的
-
创建一个 test0 文件夹,并在该文件夹中创建一个 hello.c 程序
mkdir test0 cd test0 vim hello.c
hello.c 内容如下:
#include <stdio.h> int main(void) { printf("Hello World! \n"); return 0; }
-
程序的编译过程
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链接了那些动态库 -
ELF 文件的分析
a.
一个典型的 ELF 文件包含下面几个段(1) .text:已编译程序的指令代码段
(2) .rodata:ro 代表 read only,即只读数据(譬如常数 const)
(3) .data:已初始化的 C 程序全局变量和静态局部变量
(4) .bss:未初始化的 C 程序全局变量和静态局部变量
(5) .debug:调试符号表,调试器用此段的信息帮助调试
readelf -S hello # 查看各个section(段)的信息
b.
反汇编 ELFobjdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:
gcc -o hello -g hello.c objdump -S hello
或者直接使用 objdump -D hello进行反汇编(不会有C语言源码)
-
在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代码编译生成的可执行要小得多
四、了解实际程序是如何借助第三方库函数完成代码设计
-
以游客身份体验一下即将绝迹的远古时代的 BBS (一个用键盘光标控制的终端程序)
a.
在 win10 系统中,“控制面板”–>“程序”—>“启用或关闭Windows功能”,启用 “telnet client” 和 “适用于Linux的Windows子系统”(后面会使用),然后重启。b.
打开一个 cmd命令行窗口,输入如下命令:telnet bbs.newsmth.net
以游客身份体验一下即将绝迹的远古时代的 BBS (一个用键盘光标控制的终端程序)
-
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库
五、总结
通过此次实验了解如何用 gcc 生成静态库(*.a)和动态库(*.so),并且用静/动态库链接生成可执行文件;同时一个程序的编译过程分为"预编译 —> 编译 —> 汇编 —> 链接" 四个过程。