目录
二、Gcc不是一个人在战斗。请说明gcc编译工具集中各软件的用途,了解EFF文件格式。
前言
本次作业旨在学习在Linux系统下进行程序的编译、组装过程,了解GCC编译工具的用途,并对全局变量、全局常量、局部变量、静态变量等概念的温习。
一、可执行程序的编译、组装过程
1.1 用gcc生成静态库和动态库
首先创建作业目录,通过输入“mkdir test1”完成创建,输入“cd test1”进入该目录。
通过vim文本编辑器编辑建立文件 hello.h 、hello.c 和 main.c ,如下:
程序1:hello.h 文件:
程序2:hello.c 文件:
程序3:main.c 文件:
然后通过输入“gcc -c hello.c”,将hello.c 编译成.o文件并查看.o文件是否存在,这里我们可以看到该文件目录下存在文件hello.o,故成功生成。
接下来通过.o文件来创建静态库。根据参考资料可知静态库的扩展名为.a,且需在命名时在前面加上“lib”,所以静态库文件名为“libmyhello.a”。使用“ar”命令创建静态库。如下:
然后开始在程序中使用静态库,用gcc命令生成目标文件时指明静态库名,gcc会从静态库中将函数连接到目标文件中。操作如下:
先生成main.o文件:
再生成可执行文件并运行hello程序:
再尝试从 .o 文件创建动态库文件。动态库文件名命名规范与静态库类似,需要添加前缀“lib”,但其扩展名为.so。使用gcc创建动态库。经检查发现成功建立了动态库。
“-shared”的作用是制定生成动态连接库,不用的话外部程序无法连接。
然后我们在程序中使用动态库,其方法与静态库完全一样。
但是会出现错误,这是由于找不到动态库.so文件,需将其改文件复制到/urs/lib中,找得到文件才能运行。我这里出现了移动文件权限不够的问题,可以使用sudo解决。然后就可以成功运行啦!
扩展:当静态库与动态库同名时,gcc会使用哪个库文件呢?先删除除开 .c 和 .h 外的所有文件,回到刚刚编辑完程序的样子,再进行静态库文件和动态库文件的创建:
“-fPIC”的作用是表示编译为位置独立的代码。
然后开始运行程序可知,gcc会首先使用动态库。
“-L”表示要连接的库在当前目录中。
1.2 静态库.a与.so库文件的生成与使用
同上一part一样,先建立一个作业目录保存文件,命名为“test2”,然后通过文件编辑器vim编辑生成文件:A1.c、A2.c、A.h、test.c 。内容如下:
A1.c:
A2.c:
A.h:
test.c:
首先来看静态库.a文件的生成与使用。先生成目标文件即 .o 文件,然后生成静态库.a文件。
在程序中使用静态库,得:
然后再看共享库.so文件的生成和使用。先生成目标文件,为保证文件不出错,需添加“-fpic”。
生成共享库.so文件。
使用.so文件,创建可执行程序,但此处发现有错误。
与上面一样,动态库找不到对应的.so文件,所以为避免权限不够的问题,使用sudo对文件进行拷贝,拷贝到对应的路径后再运行程序,此时成功。
1.3 实操
任务要求:
1)在第一次作业的程序代码基础进行改编,除了x2x函数之外,再扩展写一个x2y函数(功能自定),main函数代码将调用x2x和x2y ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。
先将x2x函数、x2y函数、main函数通过vim编辑器建立好,函数内容如下:
main.c 文件:
sub1.c 文件:
sub2.c 文件:
sub.h 文件:
然后使用 gcc 来生成 sub1.c 和 sub2.c 的目标文件即 .o文件。
生成静态库。
生成可执行文件。
输入“ls -l”来查看静态库的大小。
2)将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。
然后再生成动态库。
运行main程序。
查看动态库大小。
经过比较发现二者大小差不多。
二、Gcc不是一个人在战斗。请说明gcc编译工具集中各软件的用途,了解EFF文件格式。
1.Linux GCC常用命令
(1)简单编译
创建一个 test.c 文件
这个程序一步到位的编译指令是:gcc test.c -o test
但其实经过了四个阶段得出来的,即预处理(预编译)、编译、汇编、连接四个步骤。
阶段一:预处理(预编译)
代码如下:
gcc -E test.c -o test.i 或 gcc -E test.c
-E 的作用是可以让编译器在预处理之后停止并输出预处理的结果。
阶段二:编译
代码如下:
gcc -S test.i -o test.s
-S 表示在程序编译期间,生成汇编代码后,停止,-o 输出汇编代码文件。
阶段三:汇编
代码如下:
gcc -c test.s -o test.o
阶段四:连接
代码如下:
gcc test.o -o test
gcc 连接器是 gas 提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。
(2)多个程序文件的编译
通常一个完整的程序是由多个源文件组成的,相应地形成了多个编译单元,通过 gcc 指令来管理这些编译单元。假设有一个由 test1.c 和 test2.c 两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序 test,可以使用下面这条命令:
gcc test1.c test2.c -o test
(3)检错
gcc -pedantic illcode.c -o illcode
-pedantic 编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助 Linux 程序员离这个目标越来越近。
除了-pedantic 之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。比如:-wall的作用是开启大部分警告提示
gcc -Wall illcode.c -o illcode
(4)库文件连接
a. 编译成可执行文件:首先要进行编译test.c为目标文件。
gcc –c –I /usr/dev/mysql/include test.c –o test.o
b. 链接:把所有目标文件链接成可执行文件。
gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
c.强制链接时使用静态链接库:默认情况下,GCC 在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static 选项,强制使用静态链接库。
gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
2.GCC编译器背后的故事
(1)准备工作
先创建一个工作目录,然后用vim编辑器生成一个C语言编写的简单的hello.c程序,如下:
(2)编译过程
a. 预处理
①将所有的#define 删除,并且展开所有的宏定义,并且处理所有的条件预编 译指令,比如#if #ifdef #elif #else #endif 等。
②处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
③删除所有注释“//”和“/* */”。
④添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
⑤保留所有的#pragma 编译器指令,后续编译过程需要使用它们。
预处理命令如下:
gcc -E hello.c -o hello.i
b.编译
编译过程就是对预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码。编译命令如下:
gcc -S hello.i -o hello.s
c. 汇编
汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用Binutils中的汇编器 as 根据汇编指令和处理器指令的对照表一一翻译即可。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o 目标 文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。
汇编命令如下:
gcc -c hello.s -o hello.o
d. 链接
静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地拷贝到最终的可执行程序中。通过完成符号解析和重定位来创建可执行文件。
动态链接是在链接阶段加入一些描述信息,在程序执行时再从系统中把相应的动态库加载到内存中去。
(3)分析ELF文件
1* ELF文件的段
一个典型的 ELF 文件包含下面几个段:
.text:已编译程序的指令代码段。
.rodata:ro 代表 read only,即只读数据(譬如常数 const)。
.data:已初始化的 C 程序全局变量和静态局部变量。
.bss:未初始化的 C 程序全局变量和静态局部变量。
.debug:调试符号表,调试器用此段的信息帮助调试。
可以使用readelf -S 查看其各个section的信息,如下:
2* 反汇编ELF
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法。使用objdump -D 对其进行反汇编,如下:
objdump -D hello
使用 objdump -S 将其反汇编并且将其 C 语言源代码混合
gcc -o hello -g hello.c
objdump -S hello
三、编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32通过串口printf信息到上位机串口助手) 。
1、基本概念
全局常量:定义在函数外部,作用域是整个程序,其值不会改变的可供多个文件使用的量,叫做全局常量。
全局变量:定义在函数外部,作用域是整个程序,多个文件都需要使用的共同的变量,叫做全局变量。
局部变量:定义在函数内部,作用域仅在函数内部,仅供函数使用的变量,叫做局部变量。
静态变量:被关键字static修饰的变量,有全局静态变量和局部静态变量两种类型,
堆:是为动态分配预留的内存空间。从堆上分配和重新分配块没有固定模式;你可以在任何时候分配和释放它。可以被看做一棵完全二叉树的数组对象。
栈:为执行线程留出的内存空间。当函数被调用的时候,栈顶为局部变量和一些bookkeeping 数据预留块。当函数执行完毕,块就没有用了,可能在下次的函数调用的时候再被使用。
2、编程验证
代码如下:
#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
printf("hello");
printf("%d",a);
printf("\n");
}
int main( )
{
//定义局部变量
int a=2;
static int inits_local_c=2, uninits_local_c;
int init_local_d = 1;
output(a);
char *p;
char str[10] = "lyy";
//定义常量字符串
char *var1 = "1234567890";
char *var2 = "qwertyuiop";
//动态分配
int *p1=malloc(4);
int *p2=malloc(4);
//释放
free(p1);
free(p2);
printf("栈区-变量地址\n");
printf(" a:%p\n", &a);
printf(" init_local_d:%p\n", &init_local_d);
printf(" p:%p\n", &p);
printf(" str:%p\n", str);
printf("\n堆区-动态申请地址\n");
printf(" %p\n", p1);
printf(" %p\n", p2);
printf("\n全局区-全局变量和静态变量\n");
printf("\n.bss段\n");
printf("全局外部无初值 uninit_global_a:%p\n", &uninit_global_a);
printf("静态外部无初值 uninits_global_b:%p\n", &uninits_global_b);
printf("静态内部无初值 uninits_local_c:%p\n", &uninits_local_c);
printf("\n.data段\n");
printf("全局外部有初值 init_global_a:%p\n", &init_global_a);
printf("静态外部有初值 inits_global_b:%p\n", &inits_global_b);
printf("静态内部有初值 inits_local_c:%p\n", &inits_local_c);
printf("\n文字常量区\n");
printf("文字常量地址 :%p\n",var1);
printf("文字常量地址 :%p\n",var2);
printf("\n代码区\n");
printf("程序区地址 :%p\n",&main);
printf("函数地址 :%p\n",&output);
return 0;
}
(1)Ubuntu系统下
先用编辑器创建好text.c文件,并输入代码。
运行结果如下:
由上图可以看出:Ubuntu系统在栈区和堆区地址值都是从上到下逐步增大的。
(2)STM32下
编写程序,然后编译生成hex文件。
烧录后再串口软件观察结果。
可以发现,STM32在栈区和堆区地址值都是从上到下逐步减小的。
总结
本次作业内容涵括很多,静态库动态库的使用、文件的生成、GCC命令的作用以及如何使用、对全局变量和全局常量的理解以及堆栈的基本概念有了进一步的了解,也能更好的使用Ubuntu系统和STM32了。