要求:
一. 学习并掌握可执行程序的编译、组装过程。学习任务如下:
1)阅读、理解和学习材料“用gcc生成静态库和动态库.pdf”和“静态库.a与.so库文件的生成与使用.pdf”,请在Linux系统(Ubuntu)下如实仿做一遍。
2)在第一次作业的程序代码基础进行改编,除了x2x函数之外,再扩展写一个x2y函数(功能自定),main函数代码将调用x2x和x2y ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。
3)将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。
二. Gcc不是一个人在战斗。请说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:
阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍。
三. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。下图是一个Cortex-M4的存储器地址映射示意图(与Cortex-M3/stm32F10x基本相同,只存在微小差异)
目录
2)ubuntu(x86)系统和STM32(Keil)中编程验证
一. 学习并掌握可执行程序的编译、组装过程
1. 用gcc生成静态库和动态库
通常函数库被分为静态库和动态库两种,静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程 序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需 要动态库存在。
先创建一个作业目录,保存本次练习的文件
#mkdir test1
#cd test1
在创建函数库之前,先准备函数库所需要的文件
1)创建三个文件,分别为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文件
输入gcc -c hello.c ,将hello.c编译成.o文件,输入ls查看是否成功生成
3)以.o文件创建静态库
输入ar -crv libmyhello.a hello.o ,以hello.o生成lib前缀的静态库,输入ls查看是否成功生成
4)在程序中使用静态库
在程序 main.c 中,我们包含了静态库的头文件 hello.h,然后在主程序 main 中直接调用 公用函数 hello。下面先生成目标程序 hello,然后运行 hello 程序看看结果如何。
输入 gcc main.c libmyhello.a -o hello ,生成目标程序hello, 再输入./hello执行,结果如下
我们删除静态库文件试试公用函数 hello 是否真的连接到目标文件 hello 中,输入 rm libmyhello.a 删除,再重新运行hello程序,结果如下
程序照常运行,静态库中的公用函数已经连接到目标文件中了。
5) 由.o 文件创建动态库文件
输入 gcc -shared -fPIC -o libmyhello.so hello.o ,得到动态库文件libmyhello.so,输入ls查看是否成功生成
6) 在程序中使用动态库
输入 gcc -o hello main.c -L. -lmyhello 生成目标文件,然后运行它,结果如下
出现了错误,根据提示来看,是未找到动态库文件libmyhello.so ,输入 mv libmyhello.so /usr/lib 将文件libmyhello.so复制到目录/usr/lib中,再重新运行程序
若是显示权限不够,则加上sudo前缀,会提示输入计算机密码,输入即可
由此可见成功了,那当静态库和动态库同名时,gcc 命令会使用哪个库文件呢?
先删除.c和.h以外的所有文件,恢复成我们刚刚编辑完举例程序状态
输入 rm -f hello hello.o /usr/lib/libmyhello.so 删除,再输入ls查看是否成功,若是权限不够则加上sudo前缀
再来创建静态库文件 libmyhello.a 和动态库文件 libmyhello.so,代码如下
gcc -c hello.c
ar -cr libmyhello.a hello.o
gcc -shared -fPIC -o libmyhello.so hello.o
ls
然后,我们运行 gcc 命令来使用函数库 myhello 生成目 标文件 hello,并运行程序 hello.
从程序 hello 运行的结果中很容易知道,当静态库和动态库同名时,gcc 命令将优先使用动 态库,默认去连/usr/lib 和/lib 等目录中的动态库,将文件 libmyhello.so 复制到目录/usr/lib 中即可。
2.静态库.a与.so库文件的生成与使用
先创建一个作业目录,保存本次练习的文件
mkdir test2
cd test2
创建项目所需要的四个文件A1.c 、 A2.c、 A.h、 test.c ,代码如下
A1.c
#include <stdio.h>
void print1(int arg){
printf("A1 print arg:%d\n",arg);
}
A2.c
#include <stdio.h>
void print2(char *arg){
printf("A2 printf arg:%s\n", arg);
}
A.h
#ifndef A_H
#define A_H
void print1(int);
void print2(char *);
#endif
test.c
#include <stdlib.h>
#include "A.h"
int main(){
print1(1);
print2("test");
exit(0);
}
1、静态库.a 文件的生成与使用
1.1)生成目标.o文件
输入 gcc -c A1.c A2.c 将文件编译成.o文件,输入ls查看是否成功
1.2)生成静态库.a文件
输入 ar crv libafile.a A1.o A2.o 将文件编译成.a文件,输入ls查看是否成功
1.3)使用.a库文件,创造可执行文件
输入 gcc -o test test.c libafile.a 创造test程序文件,并运行吗,结果如下
2、共享库.so 文件的生成与使用
2.1)生成目标.o文件
输入 gcc -c -fpic A1.c A2.c 将文件编译成.o文件,输入ls查看是否成功
2.2) 生成共享库.so 文件
输入 gcc -shared *.o -o libsofile.so 将文件编译成.a文件,输入ls查看是否成功
2.3) 使用.so 库文件,创建可执行程序
输入 gcc -o test test.c libsofile.so 创造test程序文件,并运行吗,结果如下
发现错误,这是由于 linux 自身系统设定的相应的设置的原因,即其只在/lib and /usr/lib 下搜索对应 的.so 文件,故需将对应 so 文件拷贝到对应路径。输入 sudo cp libsofile.so /usr/lib 复制.so文件到/usr/lib文件夹下,再次运行test文件
3、x2x函数的改编
先创建一个作业目录,保存本次练习的文件
mkdir x2x
cd x2x
创建项目所需要的五个文件x2x.h、 x2x.c、x2y.h、 x2y.c、main.c ,代码如下
x2x.h
#ifndef X2X_H
#define X2X_H
float x2x(int a, int b);
#endif
x2x.c
#include <stdio.h>
float x2x(int a, int b)
{
return a*b;
}
x2y.h
#ifndef X2Y_H
#define X2Y_H
int x2y(int x, int y);
#endif
x2y.c
#include <stdio.h>
int x2y(int x, int y)
{
return x+y;
}
main.c
#include <stdio.h>
#include "x2x.h"
#include "x2y.h"
int main()
{
int a = 1, b = 2;
printf("a*b=%f\n",x2x(a,b));
printf("a+b=%d\n",x2y(a,b));
return 0;
}
1)生成目标.o文件
输入 gcc -c x2x.c x2y.c main.c 将文件编译成.o文件,输入ls查看是否成功
2)生成静态库.a文件
输入 ar -crv libafile.a x2x.o x2y.o 将文件编译成.a文件,输入ls查看是否成功
3)创造可执行文件并执行
输入 gcc main.c libafile.a -o file ,创造可执行文件file,运行输出,结果如下
4)记录文件的大小(静态库)
输入 du -sh ./* 查看文件夹各文件大小
5)生成动态库.so文件 并创建执行文件运行(动态库)
6)记录文件的大小(动态库)
由此可得,静态链接生成的libafile.a为4.0k,而动态链接生成的libafile.so为8.0k,动态库生成的文件要大于静态库生成的文件.
二. Gcc不是一个人在战斗
请说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:
阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍。
1.Linux GCC常用命令
1)简介
GCC 的意思也只是 GNU C Compiler 而已。经过了这么多年的发展,GCC 已经不仅仅能支持 C 语言;它现在还支持 Ada 语言、C++ 语言、Java 语言、Objective C 语言、Pascal 语言、COBOL 语言,以及支持函数式编程和逻辑编程的 Mercury 语言,等等。而 GCC 也不再单只是 GNU C 语 言编译器的意思了,而是变成了 GNU Compiler Collection 也即是 GNU 编译器家族的意思了。另 一方面,说到 GCC 对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在。
2)简单编译
创建一个文件teat.c ,并使用命令行进行编译,命令行和代码如下
test.c
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");
return 0;
}
编译指令
gcc test.c -o test
实质上,上述编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译 (Compilation)、汇编 (Assembly)和连接(Linking)。
2.1 预处理
- 预处理(Preprocessing):
-E
生成.i文件,将头文件的内容加载进程序中。
gcc -E test.c -o test.i 或 gcc -E test.c
2.2 编译为汇编代码
- 编译(Compilation):
-S
对.i处理,生成.s文件。生成汇编代码。
gcc -S test.i -o test.s
2.3 汇编
- 汇编(Assembly):
-c
对.s处理,生成目标文件.o
gcc -c test.s -o test.o
2.4 连接
- 连接(Linking):
-o
将程序的目标文件和所需要的其它目标文件连接起来生成可执行文件。其它目标文件包括静态连接库和动态连接库
gcc test.o -o test
3) 多个程序文件的编译
假设有两个文件test1.c和test2.c ,为了对他们进行编译并最终生成可执行程序test,可使用下面这条命令
gcc test1.c test2.c -o test
上面这条命令大致相当于依次执行下面三条命令
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc test1.o test2.o -o test
4)检错
gcc -pedantic illcode.c -o
-pedantic 编译选项并不能保证被编译程序与 ANSI/ISO C 标准的完全兼容,它仅仅只能用来帮助 Linux 程序员离这个目标越来越近。或者换句话说,-pedantic 选项能够帮助程序员发现一些不符合 ANSI/ISO C 标准的代码,但不是全部,事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的 那些情况,才有可能被 GCC 发现并提出警告。
除了-pedantic 之外,GCC 还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W 开头,其中最有价值的当数-Wall 了,使用它能够使 GCC 产生尽可能多的警告信息。
gcc -Wall illcode.c -o illcode
GCC 给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优 秀的 Linux 程序员应该尽量避免产生警告信息,使自己的代码始终保持标准、健壮的特性。所以将 警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上-Werror 选项,那 么 GCC 会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下:
gcc -Werror test.c -o test
5)库文件连接
开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助许多函数库的支 持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.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
5.1 编译成可执行文件
首先我们要进行编译 test.c 为目标文件,这个时候需要执行
gcc –c –I /usr/dev/mysql/include test.c –o test.o
5.2 链接
最后我们把所有目标文件链接成可执行文件:
gcc –L /usr/dev/mysql/lib –lmysqlclient test.o –o test
Linux 下的库文件分为两大类分别是动态链接库(通常以.so 结尾)和静态链接库(通常以.a 结尾), 二者的区别仅在于程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的。
5.3 强制链接时使用静态链接库
默认情况下, GCC 在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链 接库,如果需要的话可以在编译时加上-static 选项,强制使用静态链接库。 在/usr/dev/mysql/lib 目录下有链接时所需要的库文件 libmysqlclient.so 和 libmysqlclient.a,为了让 GCC 在链接时只用到静态链接库,可以使用下面的命令:
gcc –L /usr/dev/mysql/lib –static –lmysqlclient test.o –o test
静态链接时搜索路径顺序:
1.ld 会去找 GCC 命令中的参数-L
2.再找 gcc 的环境变量 LIBRARY_PATH
3.再找内定目录 /lib /usr/lib /usr/local/lib 这是当初 compile gcc 时写在程序内的
动态链接时、执行时搜索路径顺序:
1.编译目标代码时指定的动态库搜索路径
2.环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径
3.配置文件/etc/ld.so.conf 中指定的动态库搜索路径
4.默认的动态库搜索路径/lib
5.默认的动态库搜索路径/usr/lib
有关环境变量:
LIBRARY_PATH 环境变量:指定程序静态链接库文件搜索路径
LD_LIBRARY_PATH 环境变量:指定程序动态链接库文件搜索路径
三.GCC编译器背后的故事
GCC 不是一个人在战斗,GCC 背后其实有一堆战友。
1)准备工作
在Linux 环境中创建一个目录test0,便于记录,创建一个简单文件Hello.c,代码如下
创建目录
mkdir test0
cd test0
Hello.c
#include <stdio.h>
int main(void)
{
printf("Hello World! \n");
return 0;
}
2) 编译过程
1.预处理
(1)将所有的#define删除,并且展开所有的宏定义,处理所有的条件预编译指令,比如#if #ifdeff #elif #else #endif等。
(2)处理#include预编译指令,将被包含的文件插入到该预编译指令的位置
(3)删除所有注释//,/* */
(4)添加行号和文件标识,以便编译时产生调试时用的行号及编译错误警告行号
(5)保留所有的#pragma编译器指令,后续编译过程需要使用
使用 gcc 进行预处理的命令如下:
gcc -E Hello.c -o Hello.i
Hello.c部分代码如下
2.编译
编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及 优化后生成相应的汇编代码。
使用 gcc 进行编译的命令如下
gcc -S Hello.i -o Hello.s
Hello.s部分代码如下
3.汇编
使用 gcc 进行汇编的命令如下:
gcc -c Hello.s -o Hello.o
// 将编译生成的 hello.s 文件汇编生成目标文件 hello.o
// GCC 的选项-c 使 GCC 在执行完
//或者直接调用 as进行汇编
as -c Hell.s -o Hello.o
//使用 Binutils 中的 as 将 hello.s 文件汇编生成目标文件 注意:hello.o 目标文件为 ELF(Executable and Linkable Format)格式的可 重定向文件。
4.链接
由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动 态库文件,比如 libtest.a 和 libtest.so,gcc 链接时默认优先选择动态库,会链接 libtest.so,如果要让 gcc 选择链接 libtest.a 则可以指定 gcc 选项-static,该选项会强 制使用静态库进行链接。以 Hello World 为例:
如果使用命令“gcc hello.c -o hello”则会使用动态库进行链接,生成的 ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的动态库 (使用 Binutils 的 ldd 命令查看)如下所示
如 果 使 用 命 令 “ gcc -static hello.c -o hello”则 会 使 用 静 态 库 进 行 链 接 , 生成的 ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的 动态库(使用 Binutils 的 ldd 命令查看)如下所示
可以看出test的代码尺寸变得极大
链接器链接后生成的最终文件为 ELF 格式可执行文件,一个 ELF 可执行文件通常 被链接为不同的段,常见的段譬如.text、.data、.rodata、.bss 等段。
3)分析 ELF 文件
1.ELF 文件的段
可以使用 readelf -S 查看其各个 section 的信息如下
2.反汇编 ELF
由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包 含的指令和数据,需要使用反汇编的方法。
使用 objdump -D 对其进行反汇编如下:
使用 objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:
四.编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。
1)C程序基本概念
1. 全局变量(Global Variables):
- 全局变量在函数外部声明,可以在程序的任何地方使用。
- 全局变量的作用域是整个程序,即从声明位置到程序的末尾都可以访问。
- 全局变量的生存周期从程序开始到结束,也就是说全局变量在程序运行期间一直存在。
- 全局变量在内存中会分配静态存储空间,其地址在编译时确定,并且在整个程序运行过程中一直保持不变。
2. 局部变量(Local Variables):
- 局部变量在函数内部声明,只能在声明它的函数内部使用。
- 局部变量的作用域限定于所在的函数,只能在声明所在函数的代码块中访问。
- 局部变量的生存周期仅限于其所在函数的执行期间,当函数执行完毕后,局部变量就会被销毁。
- 局部变量通常在栈上分配存储空间,每次函数调用时都会为局部变量分配栈帧,函数返回后栈帧被销毁,局部变量的内存也被回收。
需要注意的是,全局变量和局部变量可以同名,但是存在作用域冲突的问题。当局部变量和全局变量同名时,在函数内部使用该变量时,会优先使用局部变量。
全局变量和局部变量的选择取决于变量的作用范围和使用需求。全局变量的生命周期长,可以在多个函数中共享数据,但容易造成命名冲突和隐藏性问题。局部变量的生命周期短,更节省内存空间,但只能在特定的函数内部使用。在编写程序时,应根据具体需求合理选择使用全局变量或局部变量。
3. 堆(Heap):
- 堆是用于动态内存分配的一块内存区域,它的大小可以在程序运行时动态改变。
- 堆的分配与释放是通过一些特定的函数来实现的,例如malloc()、calloc()和realloc()等,在不再需要使用分配的内存时,需要手动释放。
- 堆内存的生命周期可以比较长,可以在多个函数之间共享数据。
- 堆内存的分配操作相对较慢,因为它需要搜索可用的内存块,而且容易产生内存碎片问题。
- 堆内存通常用于动态分配较大的对象、动态创建数据结构、以及需要长时间存储的数据。
4. 栈(Stack):
- 栈是一种自动分配和释放内存的机制,用于存储局部变量、函数参数和函数返回地址等。
- 栈内存的分配和释放由编译器自动管理,当一个函数被调用时,会为其分配一块栈帧用于存储函数的局部变量和参数。函数返回后,该栈帧被销毁,相应的内存也被释放。
- 栈内存的生命周期非常短暂,仅在函数调用期间有效。
- 栈内存的分配和释放速度较快,效率高,但容量有限。
- 栈内存通常用于存储临时变量、函数调用过程中的数据以及函数调用的上下文信息。
需要注意的是,堆和栈分配的内存区域是分开的,彼此独立。在编写程序时,应根据变量的生命周期、作用域和内存需求来选择合适的内存分配方式。合理使用堆和栈可以提高程序的内存管理效率和性能。同时,应当避免堆栈内存溢出、野指针等错误,确保内存的正确分配和释放。
2)ubuntu(x86)系统和STM32(Keil)中编程验证
分别于ubuntu(x86)系统和STM32(Keil)中编写代码如下
#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] = "yaoyao";//栈
//定义常量字符串
char *var1 = "1234567890";
char *var2 = "abcdefghij";
//动态分配——堆区
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(x86)系统结果
进行.o文件编译并运行,得出ubuntu(x86)系统里的地址如下
发现stm32的栈区的地址值是从上到下减小的,堆区则是从上到下增长的。
2.STM32(Keil)结果
通过FiyMuc烧录代码,打开串口调试助手,运行结果如下
发现stm32的栈区的地址值是从上到下减小的,堆区则是从上到下增长的。
stm32的内存分配如下
从图片中可以看出ROM的地址分配是从0x8000000开始,整个大小为0x80000,这个部分用于存放代码区和文字常量区。RAM的地址分配是从0x20000000开始,其大小是0xC10000,这个区域用来存放栈、堆、全局区(.bss段、.data段)。与代码结果显示进行对比,也可以看出对应得部分得地址与设置的是相对应的。