一、用gcc生成 .a静态库和 .so动态库
所谓静态库,就是在静态编译时由编译器到指定目录寻找并且进行链接,一旦链接完成,最终的可执行程序中就包含了该库文件中的所有有用信息,包括代码段、数据段等,程序运行时将不再需要该静态库。
所谓动态库,就是在应用程序运行时,由操作系统根据应用程序的请求,动态到指定目录下寻找并装载入内存中,同时需要进行地址重定向。库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,所以在程序运行时还需 要动态库存在。
1、编辑生成例子程序
创建一个 test1 文件夹,并在该文件夹中创建三个子程序 hello.h、hello.c 和 main.c
wk@ubuntu:~$ mkdir test1 //创建test1文件夹
wk@ubuntu:~$ ls //查看一下自己的文件是否建立存在
Desktop Documents Downloads examples.desktop h hello,c hello.c hello.h hello.o main makefile Music Pictures Public sub1.c sub1.o Templates test1 Videos
wk@ubuntu:~$ cd test1 //进入test1文件
wk@ubuntu:~/test1$ vim hello.h //编辑hello.h
wk@ubuntu:~/test1$ vim hello.c // 编辑hello.c
wk@ubuntu:~/test1$ vim main.c // 编辑main.c
wk@ubuntu:~/test1$ ls //在test1文件夹中查看刚刚创建的三个子程序
hello.c hello.h main.c
写入子程序hello.h、hello.c、main.c的源代码分别如下
hello.h
#ifndef HELLO_H
#define HELLO_H
void hello(const char *name);
#endif//HELLO_H
hello.c
#include<stdio.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 文件创建的。因此,我们必须将源程序 hello.c 通过 g cc 先编译成.o 文件。
wk@ubuntu:~/test1$ gcc -c hello.c //编译完成 .o文件
wk@ubuntu:~/test1$ ls //查看
hello.o就是编译后生成的文件
3、由 .o文件创建静态库
静态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为 .a。 例如:我们将 创建的静态库名为 myhello,则静态库文件名就是 libmyhello.a。
创建静态库文件 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
//自定义的库时,main.c 还可放在-L.和 –lmyhello 之间,但是不能放在它俩之后,否则会提 示 myhello 没定义,但是是系统的库时,如 g++ -o main(-L/usr/lib) -lpthread main.cpp 就不出错。
# 方法二
gcc main.c libmyhello.a -o hello
# 方法二
gcc -o main.c // 先生成 main.o
gcc -o hello main.o libmyhello.a //再生成可执行文件
然后 ./hello 运行可执行程序
我们可尝试删除 libmyhello静态库,再次执行 hello 程序(看看公用函数hello是否真的连接到了目标文件hello中了)
rm libmyhello.a //删除libmyhello.a
./hello // 运行hello程序
这里发现程序照常运行,说明静态库中的公用函数已经连接到目标文件中,即程序运行时将不再需要该静态库了。
5、由.o 文件创建动态库文件
动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀 lib,但其 文件扩展名为.so。例如:我们将创建的动态库名为 myhello,则动态库文件名就是 libmyh ello.so。
gcc -shared -fPIC -o libmyhello.so hello.o // 生成动态库
-shared:该选项指定生成动态连接库
-fPIC:表示编译为位置独立的代码
6、在程序中使用动态库
在程序中使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含 这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明动态库名进行编译。我 们先运行 gcc 命令生成目标文件,再运行它看看结果。
两种方法生成目标文件:
gcc -o hello main.c -L. -lmyhello //方法一
gcc main.c libmyhello.so -o hello //方法二
./hello //运行
啊!在运行hello时就出现报错了
这是因为虽然连接时用的是当前目录的动态库,但是程序在运行时,会在/usr/lib 和/lib等目录中查找需要的动态库文件;若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。
怎么办呢?
既然在/usr/lib 和/lib等目录中找不到,那我们将文件 libmyhello.so 复制到目录/usr/lib 中试试
哦,这样就成功编译了。
前面这些内容可以参考这个用gcc生成静态库和动态库
二、静态库和动态库实践
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.c
#include"sub1.h"
float x2x(int a,int b)
{
return a+b;
}
sub2.h
#ifndef SUB2_H
#define SUB2_H
float x2y(int c, int d);
#endif
sub2.c
#include"sub2.h"
float x2y(int c, int d)
{
return c*d;
}
main.c
#include<stdio.h>
#include"sub1.h"
#include"sub2.h"
int main()
{
int a;
int b;
int c;
int d;
printf("input a=");
scanf("%d",&a);
printf("input b=");
scanf("%d",&b);
printf("input c=");
scanf("%d",&c);
printf("input d=");
scanf("%d",&d);
printf("%d + %d =%f",a,b,x2x(a,b));
printf("%d * %d =%f",c,d,x2y(c,d));
return 0;
}
2、用静态库文件进行链接,生成可执行文件
(1)将 sub1.c、sub2.c 编译成 .o文件
gcc -c sub1.c sub2.c
(2) .o文件创建静态库
ar -crv libsub1.a sub1.o
ar -crv libsub2.a sub2.o
(3)在程序中使用静态库
gcc main.c libsub1.a libsub2.a -o main
./main
有点小瑕疵,不影响(后面改掉)
3、用动态库文件进行链接,生成可执行文件
(1).o文件创建动态库
gcc -shared -fPIC -o libsub1.so sub1.o
gcc -shared -fPIC -o libsub2.so sub2.o
(2)在程序中使用动态库
gcc main.c libsub1.so libsub2.so -o main2
sudo mv libsub1.so /usr/lib //
sudo mv libsub2.so /usr/lib //这两步将文件 libsub1.so、libsub2.so 移动到目录/usr/lib 中;前面讲了其原因
./main2
4、查看比较两个可执行文件大小
size main1
size main2
按正常的静态动态文件大小对比,静态的要比动态的大很多。(不好意思,我这里出了点问题,等我弄清楚了,来解释修改)
三、GCC不是一个人在战斗,GCC的编译过程
由于 GCC 工具链主要是在 Linux 环境中进行使用,因此本文也将以 Linux 系统作 为工作环境。
1、编译过程
1、为了能够演示编译的过程,先创建一 个工作目录 test0,然后用文本编辑器生成一个 C 语言编写的简单 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
//静态链接
3、用 size 查看文件大小,ldd链接了那些动态库
2、FLE文件分析
链接器链接后生成的最终文件为 ELF 格式可执行文件,一个 ELF 可执行文件通常 被链接为不同的段。
1、一个典型的 ELF 文件包含下面几个段
(1) .text:已编译程序的指令代码段
(2) .rodata:ro 代表 read only,即只读数据(譬如常数 const)
(3) .data:已初始化的 C 程序全局变量和静态局部变量
(4) .bss:未初始化的 C 程序全局变量和静态局部变量
(5) .debug:调试符号表,调试器用此段的信息帮助调试
可以使用 readelf -S 查看其各个 section 的信息
readelf -S hello //查看各个section(段)的信息
2、反汇编
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包 含的指令和数据,需要使用反汇编的方法。
objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:
gcc -o hello -g hello.c
objdump -S hello
或者直接使用 objdump -D hello进行反汇编(不会有C语言源码)
objdump -D hello
3、用nasm汇编编译器编译生成执行程序
1、在ubuntu中下载安装nasm编译器
呃呃呃呃呃,虚拟机有丢丢问题,后面解决了子再来完善这部分
四、简单了解实际程序是如何借助第三方库函数完成代码设计
1、光标库(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()
nl() and nonl(): 用来决定当输入资料时, 按下 RETURN 键是否被对应为 NEWLINE 字元 ( 如 /n ). 而输出资料时, NEWLINE 字元是否被对应为 RETURN 和 LINDFEED系统预设是开启的.
echo() and noecho(): 此函式用来控制从键盘输入字元时是否将字元显示在终端机上.系统预设是开启的.
intrflush(win,bf): 呼叫 intrflush 时须传入两个值, win 为一 WINDOW 型态指标, 通常传入标准输出入萤幕 stdscr. bf 为 TRUE 或 FALSE. 当 bf 为 true 时, 当输入中断字元 ( 如 break) 时, 中断的反应将较为快速.但可能会造成萤幕的错乱.
keypad(win,bf): 呼叫 keypad 时须传入两个值, win 为一 WINDOW 型态指标, 通常传入标准输出入萤幕 stdscr. bf 为 TRUE 或 FALSE. 当开启 keypad 後, 可以使用键盘上的一些特殊字元, 如上下左右>等方向键, curses 会将这些特殊字元转换成 curses.h 内定义的一些特殊键. 这些定义的特殊键通常以 KEY_ 开头.
refresh(): refresh() 为 curses 最常呼叫的一个函式. curses 为了使萤幕输出入达最佳化, 当您呼叫萤幕输出函式企图改变萤幕上的画面时, curses 并不会立刻对萤幕做改变, 而是等到refresh() 呼叫後, 才将刚才所做的变动一次完成. 其馀的资料将维持不变. 以尽可能送最少的字元至萤幕上. 减少萤幕重绘的时间.如果是 initscr() 後第一次呼叫 refresh(), curses 将做清除萤幕的工作.
2、以游客身份体验一下即将绝迹的远古时代的 BBS(一个用键盘光标控制的终端程序)
1、设置:
在 win10 系统中,“控制面板”–>“程序”—>“启用或关闭Windows功能”;启用 “telnet client” 和"适用于Linux的Windows子系统"
2、以游客身份体验远古时代的BBS
打开一个 cmd命令行窗口,输入如下命令:
telnet bbs.newsmth.net
进入后的界面
3、Linux 环境下C语言编译实现贪吃蛇游戏
1、安装curses库
sudo apt-get install libncurses5-dev
然后可以通过whereis 命令头文件和库文件查看被安装到哪些目录中:
2、Linux 环境下C语言编译实现贪吃蛇游戏
mkdir t_snake //新建一个文件夹
cd t_snake //进入文件
vim mysnake.c //进入编辑,代码见后面链接
gcc mysnake.c -lcurses -o mysnake //编译链接生成可执行文件
./mysnacke //执行程序
mysnake.c 的内容请参考:Linux 环境下C语言编译实现贪吃蛇游戏
程序运行效果如下: