文章目录
一、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编译的四个步骤,尝试了汇编编译,弄清楚了这几种编译方式的区别和各自的特点。第一次使用函数库实现了一个游戏。感觉收获颇丰。