编译器背后的故事
- 一. 可执行程序是如何被组装的
- 1)gcc生成静态库和动态库及使用
- 2)目标文件与静态库文件的链接并生成可执行程序
- 3)目标文件与静态库文件的链接并生成可执行程序
- 二.说明gcc编译工具集中各软件的用途
- 1)Linux GCC常用命令
- 2)nasm汇编编译器编译生成执行程序
- 三.实际程序如何借助第三方库函数完成代码设计
- 1)光标库(curses)的主要函数功能
- 2)远古时代的 BBS ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201014222638924.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NzIyMzU2MQ==,size_16,color_FFFFFF,t_70#pic_center)
- 3)安装curses库
- 4)Linux 环境下C语言编译贪吃蛇游戏
- 总结
一. 可执行程序是如何被组装的
一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接。其中,编译是主要部分,其中又分为六个部分:词法分析、语法分析、语义分析、中间代码生成、目标代码生成和优化。链接中,分为静态链接和动态链接
1)gcc生成静态库和动态库及使用
(1)创建目录
先创建一个作业目录test1,在终端输入
mkdir test1
cd test1
再输入
touch hello.h
touch hello.c
touch main.c
就创建了hello.h,hello.c和main.c三个文件
(2)编辑文件
用gedit文本编辑器编辑生成的三个文件。
在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;
}
(3)将 hello.c 编译成.o 文件
在终端输入
gcc -c hello.c
将hello.c生成hello.o文件,可以输入 ls 查看
(4)由.o 文件创建静态库
在终端输入
ar -crv libmyhello.a hello.o
创建静态库文件libmyhello.a。可用 ls 查看
(5)在程序中使用静态库
输入代码
gcc main.c libmyhello.a -o hello
生成目标程序 hello,在运行 hello 程序,结果如下
再删除静态库文件,判断公用函数 hello 是否真的连接到目标文件 hello 中了。
输入
rm libmyhello.a
在运行hello程序,结果如下
可以得出:静态库中的公用函数已经连接到目标文件中了
(6)由.o 文件创建动态库文件
在终端输入
gcc -shared -fPIC -o libmyhello.so hello.o
得到动态库文件 libmyhello.so。在输入ls查看
(7)在程序中使用动态库
运行 gcc 命令生成目标文件
gcc -o hello main.c -L. -lmyhello
然后输入 ./hello 运行生成文件,结果出现
这是因为程序在运行时, 在/usr/lib 和/lib 等目录中没有查找到查找需要的动态库文件。只需将将文件 libmyhello.so 复制到目录/usr/lib 中,在终端输入代码
sudo mv libmyhello.so /usr/lib
(sudo是使用管理员权限),否则将还是错的。接着输入虚拟机的密码,就可以得到
考虑:当静态库和动态库同名时,gcc 命令会使用哪个库文件
先删除除.c 和.h 外的所有文件,输入
sudo rm -f hello hello.o /usr/lib/libmyhello.so
再创建静态库文件 libmyhello.a 和动态库文件 libmyhello.so,在输入
gcc -fpic -c hello.c
ar -cr libmyhello.a hello.o
gcc -shared -fPIC -o libmyhello.so hello.o
输入ls,查看
运行 gcc 命令来使用函数库 myhello 生成目 标文件 hello,并运行程序 hello。输入
gcc -o hello main.c -L. –lmyhello
./hello
结果如下
所以可知,当静态库和动态库同名时,gcc 命令将优先使用动 态库,默认去连/usr/lib 和/lib 等目录中的动态库,将文件 libmyhello.so 复制到目录/usr/lib 中即可
2)目标文件与静态库文件的链接并生成可执行程序
(1)创建并编辑文件
输入以下代码,创建main1.c,sub1.c和sub2.c
touch main1.c
touch sub1.c
touch sub2.c
在输入
vi main1.c
编辑main1.文件,在文件内输入代码
#include "sub1.c"
#include "sub2.c"
#include <stdio.h>
int main()
{
int a,b;
scanf("%d%d",&a,&b);
printf("%f\n",x2x(a,b));
printf("%f",x2y(a,b));
return 0;
}
输入
vi sub1.c
编辑sub1.c文件,在文件内输入
float x2x(int a,int b)
{
return a/b;
}
同理,编辑sub2.c,在文本内输入
float x2y(int a,int b)
{
return a+b;
}
(2)用gcc分别编译为3个.o 目标文件
输入代码
gcc -fpic -c main1.c
gcc -fpic -c sub1.c
gcc -fpic -c sub2.c
生成了main.o,sub1.o和sub2.o文件
(3)用ar工具生成1个 .a 静态库文件
输入代码
ar -crv libmysub1.a sub1.o sub2.o
就将sub1.o和sub2.o两个文件生成了一个名为libmysub1.a的静态库文件
(4)main1函数的目标文件与静态库文件链接
输入代码
gcc main1.c libmysub1.a -o sub1
生成了可执行程序文件 sub1,运行sub1
(5)记录文件大小
3)目标文件与静态库文件的链接并生成可执行程序
(1)生成 .so 动态库文件
输入代码
gcc -shared -fPIC -o libmysub1.so sub1.o sub2.o
生成一个动态库文件libmysub1.so
(2)main函数的目标文件与动态库文件链接
输入代码
gcc -o sub1 main1.c -L. -lmysub1
在运行可执行文件sub1,结果如下
(3)记录文件大小
相同的静态可执行文件和动态可执行文件的大小是一样的
二.说明gcc编译工具集中各软件的用途
- readelf:elf 文件格式分析工具
这个工具和 objdump 命令提供的功能类似,但是它显示的信息更为具体,并且它不依赖 BFD 库( BFD 库是一个 GNU 项目,它的目标就是希望通过一种统一的接口来处理不同的目标文件);
ELF 文件类型 ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用。可以说,ELF 是构成众多 xNIX 系统的基础之一。
ELF文件有三种类型:
可重定位的对象文件(Relocatable file) 由汇编器汇编生成的 .o 文件可执行的对象文件(Executable file) 可执行应用程序可被共享的对象文件(Shared object file) 动态库文件,也即 .so 文件 - size
size 工具,就是列出程序文件中各段的大小。默认情况下,对于每个目标文件或者一个归档文件中的每个模块只产生一行输出。命令使用格式:size [ option … ] [ object … ] - strings
strings 工具在对象文件或二进制文件中查找可打印的字符串。字符串是4个或更多可打印字符的任意序列,以换行符或空字符结束。 strings 工具对识别随机对象文件很有用。语法:strings [ -a ] [ - ] [ -o ] [ -t Format ] [ -n Number ] [ -Number ] [ file … ] - strip
strip 工具通过除去绑定程序和符号调试程序使用的信息,减少扩展公共对象文件格式(XCOFF)的对象文件的大小。
语法 strip [ -V ] [ -r [ -l ] | -x [ -l ] | -t | -H | -e | -E ] [ -X {32 |64 |32_64 }] [ – ] File …
strip 命令减少 XCOFF 对象文件的大小。
strip 命令从 XCOFF 对象文件中有选择地除去行号信息、重定位信息、调试段、typchk 段、注释段、文件头以及所有或部分符号表。 一旦使用该命令,则很难调试文件的符号;因此,通常应该只在已经调试和测试过的生成模块上使用 strip 命令。使用 strip 命令减少对象文件所需的存储量开销。
对于每个对象模块,strip 命令除去给出的选项所指定的信息。对于每个归档文件,strip 命令从归档中除去全局符号表。可以使用 ar -s 命令将除去的符号表恢复到归档文件或库文件中。没有选项的 strip 命令除去行号信息、重定位信息、符号表、调试段、typchk 段和注释段。
汇编语言的格式:
一,指令语句
【标号】: 指令助记符 【操作数,。。。,操作数】【;注释】
例如:MOV AX,DSEG
;数据段段值送AX寄存器
NOT TEMP
二,伪指令语句:宏汇编中使用
【名字】 伪指令定义符【参数,。。。参数】;注释】
三,标识符
指令语句中的标号和伪指令语句中的符号名统称为标识符,规则如下:
。字符个数1~31
。第一个字符必须是字母或特殊字符(?@_.¥)
从第二个字符开始,可以是字母/数字/特殊字符;
。标识符不能与系统专用保留字相同
1)Linux GCC常用命令
(1)编译过程是分为四个阶段进行的
1>预处理
首先创建一个test.c的文件,然后编译文件
int main(void)
{
printf("Hello World!\n");
return 0;
}
输入代码
gcc -E test.c -o test.i
输出 test.i 文件中存放着 test.c 经预处理之后的代码
2>编译为汇编代码
输入代码
gcc -S test.i -o test.s
直接对生成的 test.i 文件编译,生成汇编代码
3>汇编
输入代码
gcc -c test.s -o test.o
汇编代码文件 test.s,gas 汇编器负责将其编译为目标文件
4>链接
输入代码
gcc test.o -o test
将test.o与C标准输入输出库进行连接,最终生成程序 test。执行test
(2)库文件连接
1>使用动态库连接
输入代码
gcc test.c -o test
查看文件大小
可执行文件链接的动态库
2>使用静态库连接
输入代码
gcc -static test.c -o test
查看文件大小,(可以看出 text 的代码尺寸 变得极大)
可执行文件链接的静态库(说明没有链接动态库)
(3)分析 ELF 文件
1>ELF 文件的段
输入代码
readelf -S test
部分截图
2>反汇编 ELF
输入代码
objdump -D test
使用 objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:
输入代码
gcc -o test -g test.c
objdump -S test
部分截屏
2)nasm汇编编译器编译生成执行程序
(1)安装nasm
在终端输入
sudo apt-get install nasm
就可以安装nasm
(2)“hello.asm”编译生成可执行程序
输入代码
nasm -f elf64 hello.asm
将hello.asm 生成 目标文件hello.o。在输入代码
ld -s -o hello hello.o
用ld 链接器把 hello.o 生成可执行程序 hello,结果如下
文件的对比:
C代码的编译生成的程序大
三.实际程序如何借助第三方库函数完成代码设计
开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助许多函数库的支持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(so、或lib、dll)的集合。。虽然Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录下;Windows所使用的库文件主要放在Visual Stido的目录下的include和lib,以及系统文件夹下。但也有的时候,我们要用的库不再这些目录下,所以GCC在编译时必须用自己的办法来查找所需要的头文件和库文件。
例如我们的程序test.c是在linux上使用c连接mysql,这个时候我们需要去mysql官网下载MySQL Connectors的C库,下载下来解压之后,有一个include文件夹,里面包含mysql connectors的头文件,还有一个lib文件夹,里面包含二进制so文件libmysqlclient.so
其中inclulde文件夹的路径是/usr/dev/mysql/include,lib文件夹是/usr/dev/mysql/lib
1)光标库(curses)的主要函数功能
(1)refresh函数
函数定义: int refresh(void);
说明: curses最常用的一个函数
在调用屏幕输出函数试图改变屏幕上的画面时,curses并不会立刻对屏幕做改变,而是等到refresh()调用后,才将刚才所做的变动一次完成其余信息维持不变,以尽可能送最少字符发送至屏幕上,减少屏幕重绘时间如果是initscr()后第一次调用refresh(),curses将做清除屏幕的工作
(2)从屏幕读取基本函数
函数定义:chtype inch(void);
返回光标当前位置的字符及其属性
int instr(char *string);
将返回内容写到字符数组中
int innstr(char *string, int number_of_characters);
将返回内容写到字符数组中,可以指定返回字符的个数
(3)清除屏幕
int erase(void);
在每个屏幕位置写上空白字符
int clear(void);
与erase()类似,也是清屏,但通过调用clearok函数来强制重现屏幕原文
clearok函数强制执行清屏操作,并在下次调用refresh函数时重现屏幕原文
int clrtobot(void);
清除当前光标所在行下面的所有行,包括当前光标所在行中的光标位置右侧直到行尾的内容
int clrtoeol(void);
清除当前光标所在行中光标位置右边至行尾的内容
2)远古时代的 BBS
输入guest继续
多个项目,可以随意选择
3)安装curses库
在终端输入
sudo apt-get install libncurses5-dev
安装curses库
1>系统标准头文件位置: /usr/include下,以及安装库的头文件位置:/usr/local/include/
如 #include<linux/can.h> 对应 /usr/include/linux/can.h
#include<stdio.h> 对应 /usr/include/stdio.h
#include <libusb-1.0/libusb.h> 对应 /usr/local/include/libusb-1.0/libusb.h
2>系统标准库文件位置:/lib /usr/lib
-用户安装库位置: /usr/local/lib
默认只搜索标准c语言库,对于系统标准库中的其他库以及安装库,需要在编译时指定库名。对于非系统标准库还需通过-L来指定库文件位置。
4)Linux 环境下C语言编译贪吃蛇游戏
在终端输入
gedit mysnake1.0.c
编辑mysnake1.0.c文件
#include <stdio.h>
#include <stdlib.h>
#include <curses.h>
#include <signal.h>
#include <sys/time.h>
#define NUM 60
struct direct //用来表示方向的
{
int cx;
int cy;
};
typedef struct node //链表的结点
{
int cx;
int cy;
struct node *back;
struct node *next;
}node;
void initGame(); //初始化游戏
int setTicker(int); //设置计时器
void show(); //显示整个画面
void showInformation(); //显示游戏信息(前两行)
void showSnake(); //显示蛇的身体
void getOrder(); //从键盘中获取命令
void over(int i); //完成游戏结束后的提示信息
void creatLink(); //(带头尾结点)双向链表以及它的操作
void insertNode(int x, int y);
void deleteNode();
void deleteLink();
int ch; //输入的命令
int hour, minute, second; //时分秒
int length, tTime, level; //(蛇的)长度,计时器,(游戏)等级
struct direct dir, food; //蛇的前进方向,食物的位置
node *head, *tail; //链表的头尾结点
int main()
{
initscr();
initGame();
signal(SIGALRM, show);
getOrder();
endwin();
return 0;
}
void initGame()
{
cbreak(); //把终端的CBREAK模式打开
noecho(); //关闭回显
curs_set(0); //把光标置为不可见
keypad(stdscr, true); //使用用户终端的键盘上的小键盘
srand(time(0)); //设置随机数种子
//初始化各项数据
hour = minute = second = tTime = 0;
length = 1;
dir.cx = 1;
dir.cy = 0;
ch = 'A';
food.cx = rand() % COLS;
food.cy = rand() % (LINES-2) + 2;
creatLink();
setTicker(20);
}
//设置计时器(这个函数是书本上的例子,有改动)
int setTicker(int n_msecs)
{
struct itimerval new_timeset;
long n_sec, n_usecs;
n_sec = n_msecs / 1000 ;
n_usecs = ( n_msecs % 1000 ) * 1000L ;
new_timeset.it_interval.tv_sec = n_sec;
new_timeset.it_interval.tv_usec = n_usecs;
n_msecs = 1;
n_sec = n_msecs / 1000 ;
n_usecs = ( n_msecs % 1000 ) * 1000L ;
new_timeset.it_value.tv_sec = n_sec ;
new_timeset.it_value.tv_usec = n_usecs ;
return setitimer(ITIMER_REAL, &new_timeset, NULL);
}
void showInformation()
{
tTime++;
if(tTime >= 1000000) //
tTime = 0;
if(1 != tTime % 50)
return;
move(0, 3);
//显示时间
printw("time: %d:%d:%d %c", hour, minute, second);
second++;
if(second > NUM)
{
second = 0;
minute++;
}
if(minute > NUM)
{
minute = 0;
hour++;
}
//显示长度,等级
move(1, 0);
int i;
for(i=0;i<COLS;i++)
addstr("-");
move(0, COLS/2-5);
printw("length: %d", length);
move(0, COLS-10);
level = length / 3 + 1;
printw("level: %d", level);
}
//蛇的表示是用一个带头尾结点的双向链表来表示的,
//蛇的每一次前进,都是在链表的头部增加一个节点,在尾部删除一个节点
//如果蛇吃了一个食物,那就不用删除节点了
void showSnake()
{
if(1 != tTime % (30-level))
return;
//判断蛇的长度有没有改变
bool lenChange = false;
//显示食物
move(food.cy, food.cx);
printw("@");
//如果蛇碰到墙,则游戏结束
if((COLS-1==head->next->cx && 1==dir.cx)
|| (0==head->next->cx && -1==dir.cx)
|| (LINES-1==head->next->cy && 1==dir.cy)
|| (2==head->next->cy && -1==dir.cy))
{
over(1);
return;
}
//如果蛇头砬到自己的身体,则游戏结束
if('*' == mvinch(head->next->cy+dir.cy, head->next->cx+dir.cx) )
{
over(2);
return;
}
insertNode(head->next->cx+dir.cx, head->next->cy+dir.cy);
//蛇吃了一个“食物”
if(head->next->cx==food.cx && head->next->cy==food.cy)
{
lenChange = true;
length++;
//恭喜你,通关了
if(length >= 50)
{
over(3);
return;
}
//重新设置食物的位置
food.cx = rand() % COLS;
food.cy = rand() % (LINES-2) + 2;
}
if(!lenChange)
{
move(tail->back->cy, tail->back->cx);
printw(" ");
deleteNode();
}
move(head->next->cy, head->next->cx);
printw("*");
}
void show()
{
signal(SIGALRM, show); //设置中断信号
showInformation();
showSnake();
refresh(); //刷新真实屏幕
}
void getOrder()
{
//建立一个死循环,来读取来自键盘的命令
while(1)
{
ch = getch();
if(KEY_LEFT == ch)
{
dir.cx = -1;
dir.cy = 0;
}
else if(KEY_UP == ch)
{
dir.cx = 0;
dir.cy = -1;
}
else if(KEY_RIGHT == ch)
{
dir.cx = 1;
dir.cy = 0;
}
else if(KEY_DOWN == ch)
{
dir.cx = 0;
dir.cy = 1;
}
setTicker(20);
}
}
void over(int i)
{
//显示结束原因
move(0, 0);
int j;
for(j=0;j<COLS;j++)
addstr(" ");
move(0, 2);
if(1 == i)
addstr("Crash the wall. Game over");
else if(2 == i)
addstr("Crash itself. Game over");
else if(3 == i)
addstr("Mission Complete");
setTicker(0); //关闭计时器
deleteLink(); //释放链表的空间
}
//创建一个双向链表
void creatLink()
{
node *temp = (node *)malloc( sizeof(node) );
head = (node *)malloc( sizeof(node) );
tail = (node *)malloc( sizeof(node) );
temp->cx = 5;
temp->cy = 10;
head->back = tail->next = NULL;
head->next = temp;
temp->next = tail;
tail->back = temp;
temp->back = head;
}
//在链表的头部(非头结点)插入一个结点
void insertNode(int x, int y)
{
node *temp = (node *)malloc( sizeof(node) );
temp->cx = x;
temp->cy = y;
temp->next = head->next;
head->next = temp;
temp->back = head;
temp->next->back = temp;
}
//删除链表的(非尾结点的)最后一个结点
void deleteNode()
{
node *temp = tail->back;
node *bTemp = temp->back;
bTemp->next = tail;
tail->back = bTemp;
temp->next = temp->back = NULL;
free(temp);
temp = NULL;
}
//删除整个链表
void deleteLink()
{
while(head->next != tail)
deleteNode();
head->next = tail->back = NULL;
free(head);
free(tail);
}
在输入
cc mysnake1.0.c -lcurses -o mysnake1.0
./mysnake1.0
出现
这是吃了一个@之后的
这是撞墙死了的结果
总结
通过学习和上网查阅,我叫了解到了静态库封装在软件目录中,与软件本体共生,使用静态库是为了管理软件本身。动态库则需要随着软件的移植,在当前系统中的库中进行配置。在程序编译的时候,检索的是当前系统的库,是否有对应的动态库文件。因此,同一个库文件资源,可以供多个程序共享使用,可以省下许多内存空间。
通过这次作业,我深刻认识到要增加自己Linux的技能,只有通过实践不断地练习,才可以熟练的掌握虚拟机的技能。未来的路还有很长,希望自己可以保持初心,坚持不懈 !