编译器背后的故事

编译器背后的故事

一. 可执行程序是如何被组装的

一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接。其中,编译是主要部分,其中又分为六个部分:词法分析、语法分析、语义分析、中间代码生成、目标代码生成和优化。链接中,分为静态链接和动态链接

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编译工具集中各软件的用途

  1. 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 文件
  2. size
    size 工具,就是列出程序文件中各段的大小。默认情况下,对于每个目标文件或者一个归档文件中的每个模块只产生一行输出。命令使用格式:size [ option … ] [ object … ]
  3. strings
    strings 工具在对象文件或二进制文件中查找可打印的字符串。字符串是4个或更多可打印字符的任意序列,以换行符或空字符结束。 strings 工具对识别随机对象文件很有用。语法:strings [ -a ] [ - ] [ -o ] [ -t Format ] [ -n Number ] [ -Number ] [ file … ]
  4. 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的技能,只有通过实践不断地练习,才可以熟练的掌握虚拟机的技能。未来的路还有很长,希望自己可以保持初心,坚持不懈 !

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值