编译器背后的故事
一、可执行程序的组装
1) 一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接。
其中,编译是主要部分,其中又分为六个部分:词法分析、语法分析、语义分析、中间代码生成、目标代码生成和优化。
链接中,分为静态链接和动态链接,本文主要是静态链接。
1.预编译:主要处理源代码文件中的以“#”开头的预编译指令。
2.编译:把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
3.汇编:将汇编代码转变成机器可以执行的指令(机器码文件)。
4.链接
1.用gcc生成静态库和动态库
创建静态库
1.编辑生成例子程序 hello.h、hello.c 和 main.c
程序 1: hello.h
#ifndef HELLO_H
#define HELLO_H
void hello(const char *name);
#endif //HELLO_H
程序 2: hello.c
#include <stdio.h>
void hello(const char *name)
{
printf("Hello %s!\n", name);
}
程序 3: main.c
#include "hello.h"
int main()
{
hello("everyone");
return 0;
}
2.将 hello.c 编译成.o 文件
gcc -c hello.c
ls
3.由.o文件创建静态库
ar -crv libmyhello.a hello.o
ls
4.在程序中使用静态库
一:
gcc -o hello main.c -L. -lmyhello
./hello
二:
gcc main.c libmyhello.a -o hello
./hello
三:
gcc -c main.c //生成main.o
gcc -o hello main.o libmyhello.a // 生成可执行文件
./hello
删除静态库文件测试公用函数是否连接到目标文件hello中
rm libmyhello.a
./hello
5.由.o文件创建动态库文件
gcc -shared -fPIC -o libmyhello.so hello.o
ls
6.在程序中使用动态库
gcc -o hello main.c -L. -lmyhello
./hello
2.静态库.a与.so库文件的生成与使用
创建目录
mkdir test2 //创建test2目录
cd test2
创建文件
touch A1.c
touch A2.c
touch A.h
touch test.c
在文件里输入相关代码
#include <stdio.h>
void print1(int arg)
{
printf("A1 print arg:%d\n",arg);
}
#include <stdio.h>
void print2(char *arg)
{
printf("A2 printf arg:%s\n", arg);
}
#ifndef A_H
#define A_H
void print1(int);
void print2(char *);
#endif
#include <stdlib.h>
#include "A.h"
int main()
{
print1(1);
print2("test");
exit(0);
}
静态库.a文件的生成与使用
生成目标文件
gcc -c A1.c A2.c
生成静态库.a文件
ar crv libafile.a A1.o A2.o
使用.a库文件,创建可执行程序
gcc -o test test.c libafile.a
./test
生成目标文件
gcc -c -fpic A1.c A2.c
生成共享库.so文件
gcc -shared *.o -o libsofile.so
使用.so文件,创建可执行程序
gcc -o test test.c libsofile.so
./test
拷贝.so文件
sudo cp libsofile.so /usr/lib
继续执行即可运行程序
二.改编第一次作业程序生成静态库和动态库文件并进行比较
打开第一次作业目录,改编代码(添加x2y函数)
main1.c
#include"sub1.h"
int main()
{
int m=1,n=2;
printf("%.3f\n",x2x(m,n));
printf("%.3f\n",x2y(m,n));
return 0;
}
sub1.c
#include"sub1.h"
float x2x(int a,int b)
{
float sum;
sum=a+b;
return sum;
}
sub2.c
#include"sub1.h"
float x2y(int a,int b)
{
float sum;
sum=b-a;
return sum;
}
sub1.h
#include<stdio.h>
float x2x(int a,int b);
float x2y(int a,int b);
生成.o文件(由于之前已生成main.o,sub1.o文件,故我这里直接生成sub2.o文件)
gcc -c sub2.c
将x2x,x2y目标文件用ar工具生成一个.a静态库文件
ar -crv libsub.a sub1.o sub2.o
main目标文件与该静态库文件连接
gcc -o main1 main1.c -L. libsub.a
生成动态库文件并比较大小
gcc -shared -fPIC -o libsub.so sub1.o sub2.o
在程序中使用动态库
gcc -o main2 main1.c -L. libsub.so
三.Gcc相关知识
1.Gcc常用编译代码
(1)addr2line:用来将程序地址转换成其所对应的程序源文件及所对应的代码
行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对应的源代码位置。
(2)as:主要用于汇编
(3)ld:主要用于链接
(4)ar:主要用于创建静态库。
(5)ldd:可以用于查看一个可执行程序依赖的共享库
(6)objcopy:将一种对象文件翻译成另一种格式,譬如将.bin 转换成.elf、或
者将.elf 转换成.bin 等。
(7)objdump:主要的作用是反汇编。
(8)readelf:显示有关 ELF 文件的信息
(9)size:列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小
等。
2.Gcc编译过程
1.预处理
预处理的过程主要包括以下过程:
(1) 将所有的#define 删除,并且展开所有的宏定义,并且处理所有的条件预编
译指令,比如#if #ifdef #elif #else #endif 等。
(2) 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
(3) 删除所有注释“//”和“/* */”。
(4) 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
(5) 保留所有的#pragma 编译器指令,后续编译过程需要使用它们。
2.编译
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及
优化后生成相应的汇编代码。
3.汇编
汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o
的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相
对于编译过程比较简单,通过调用 Binutils 中的汇编器 as 根据汇编指令和处理
器指令的对照表一一翻译即可。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o 目标
文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部
分了,但是在链接之前还不能执行。
4.链接
链接也分为静态链接和动态链接,其要点如下:
(1) 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行
文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链
接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完
成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和
重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。
(2) 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统
中把相应动态库加载到内存中去。
3.分析 ELF 文件
1.ELF文件的段
ELF 文件格式如下图所示,位于 ELF Header 和 Section Header Table 之间的都
是段(Section)。一个典型的 ELF 文件包含下面几个段:
.text:已编译程序的指令代码段。
.rodata:ro 代表 read only,即只读数据(譬如常数 const)。
.data:已初始化的 C 程序全局变量和静态局部变量。
.bss:未初始化的 C 程序全局变量和静态局部变量。
.debug:调试符号表,调试器用此段的信息帮助调试。
2.反汇编 ELF
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包
含的指令和数据,需要使用反汇编的方法。
使用 objdump -D 对其进行反汇编
四.as汇编编译器执行程序
1.安装nasm
在Ubuntu中输入
sudo apt install nasm
2.使用nasm进行编译
代码如下:
; 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 ; 调用内核功能
nasm -f elf64 hello.asm
ld -s -o hello hello.o
./hello
3.编译生成”hello world”C代码程序大小并与nasm进行对比
创建test.c并进行编译
代码如下:
#include<stdio.h>
void main()
{
printf("Hello, world!\n");
}
nasm代码大小
五.Linux第三方库函数
1.curses主要功能
curses是一个在Linux/Unix下广泛应用的图形函数库,作用是可以在终端内绘制简单的图形用户界面。
2.一些基本函数及功能
initscr():初始化curses库和ttty
(在开始curses编程之前,必须使用initscr()这个函数来开启curses模式)
endwin():关闭curses并重置tty
(结束curses编程时,最后调用的一个函数)
move(y,x):将游标移动至 x,y 的位置
getch():从键盘读取一个字元。(注意!传回的是整数值)
refresh():使屏幕按照你的意图显示。比较工作屏幕和真实屏幕的差异,然后refresh通过终端驱动送出那些能使真实屏幕于工作屏幕一致的字符和控制码。(工作屏幕就像磁盘缓存,curses中的大部分的函数都只对它进行修改)
3.以游客身份体验远古时代的 BBS
在 win10 系统中,“控制面板”–>“程序”—>“启用或关闭Windows功能”,启用 “telnet client” 和"适用于Linux的Windows子系统"(后面会使用)。 然后打开一个cmd命令行窗口,命令行输入 telnet bbs.newsmth.net
4.安装curses
5.用gcc编译生成一个终端游戏
创建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);
}
执行程序