一、学习并掌握可执行程序的编译、组装过程
用gcc生成 .a静态库和 .so动态库
准备举例用的源程序,并将函数库的源程序编译成.o 文件
(1)
第
1
步:编辑生成例子程序
hello.h
、
hello.c
和
main.c
。
先创建一个作业目录,保存本次练习的文件。
#mkdir test1
#cd test1
然后用
vim
、
nano
或
gedit
等文本编辑器编辑生成所需要的
3
个文件。
hello.c(
见程序
2)
是函数库的源程序,其中包含公用函数
hello
,该函数将在屏幕上输出
"Hello
XXX!"
。
hello.h(
见程序
1)
为该函数库的头文件。
main.c(
见程序
3)
为测试库文件的主程序,
在主程序中调用了公用函数
hello
。
程序
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)
第
2
步:将
hello.c
编译成
.o
文件。
无论静态库,还是动态库,都是由
.o
文件创建的。因此,我们必须将源程序
hello.c
通过
g
cc
先编译成
.o
文件。在系统提示符下键入以下命令得到
hello.o
文件。
# gcc -c hello.c
#
我们运行
ls
命令看看是否生存了
hello.o
文件。
# ls
hello.c hello.h hello.o main.c
#
在
ls
命令结果中,我们看到了
hello.o
文件,本步操作完成。
下面我们先来看看如何创建静态库,以及使用它。
(3)
第
3
步:由
.o
文件创建静态库。
静态库文件名的命名规范是以
lib
为前缀,紧接着跟静态库名,扩展名为
.a
。例如:我们将
创建的静态库名为
myhello
,则静态库文件名就是
libmyhello.a
。在创建和使用静态库时,
需要注意这点。创建静态库用
ar
命令。在系统提示符下键入以下命令将创建静态库文件
libmyhello.a
。
# ar -crv libmyhello.a hello.o
#
我们同样运行
ls
命令查看结果:
# ls
hello.c hello.h hello.o libmyhello.a main.c
#
ls
命令结果中有
libmyhello.a
。
(4)
第
4
步:在程序中使用静态库。
静态库制作完了,如何使用它内部的函数呢?只需要在使用到这些公用函数的源程序中包
含这些公用函数的原型声明,然后在用
gcc
命令生成目标文件时指明静态库名,
gcc
将会从
静态库中将公用函数连接到目标文件中。注意,
gcc
会在静态库名前加上前缀
lib
,然后追
加扩展名
.a
得到的静态库文件名来查找静态库文件。
在程序
3:main.c
中,我们包含了静态库的头文件
hello.h
,然后在主程序
main
中直接调用
公用函数
hello
。下面先生成目标程序
hello
,然后运行
hello
程序看看结果如何。
方法一:
# gcc -o hello main.c -L. –lmyhello
自定义的库时,
main.c
还可放在
-L.
和
–lmyhello
之间,但是不能放在它俩之后,否则会提
示
myhello
没定义,但是是系统的库时,如
g++ -o main
(
-L/usr/lib
)
-lpthread main.cpp
就不出错。
方法二:
#gcc main.c libmyhello.a -o hello
方法三:
先生成
main.o
:
gcc -c main.c
再生成可执行文件:
gcc -o hello main.o libmyhello.a
动态库连接时也可以这样做。
# ./hello
Hello everyone!
#
我们删除静态库文件试试公用函数
hello
是否真的连接到目标文件
hello
中了。
# rm libmyhello.a
rm: remove regular file `libmyhello.a'? y
# ./hello
Hello everyone!
#
程序照常运行,静态库中的公用函数已经连接到目标文件中了。
我们继续看看如何在
Linux
中创建动态库。我们还是从
.o
文件开始。
(5)
第
5
步:由
.o
文件创建动态库文件。
动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀
lib
,但其
文件扩展名为
.so
。例如:我们将创建的动态库名为
myhello
,则动态库文件名就是
libmyh
ello.so
。用
gcc
来创建动态库。
在系统提示符下键入以下命令得到动态库文件
libmyhello.so
。
# gcc -shared -fPIC -o libmyhello.so hello.o
(
-o
不可少)
#
我们照样使用
ls
命令看看动态库文件是否生成。
# ls
hello.c hello.h hello.o libmyhello.so main.c
#
(6)
第
6
步:在程序中使用动态库;
在程序中使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含
这些公用函数的原型声明,然后在用
gcc
命令生成目标文件时指明动态库名进行编译。我
们先运行
gcc
命令生成目标文件,再运行它看看结果。
# gcc -o hello main.c -L. -lmyhello
(
或
#gcc main.c libmyhello.so -o hello
不会出错(没有
libmyhello.so
的话,会出错),但是
接下来
./hello
会提示出错,因为虽然连接时用的是当前目录的动态库,但是运行时,是到
/usr/lib
中找库文件的,将文件
libmyhello.so
复制到目录
/usr/lib
中就
OK
了
)
# ./hello
./hello: error while loading shared libraries: libmyhello.so: cannot open shar
ed object file: No such file or directory
#
哦!出错了。快看看错误提示,原来是找不到动态库文件
libmyhello.so
。程序在运行时,
会在
/usr/lib
和
/lib
等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提
示类似上述错误而终止程序运行。我们将文件
libmyhello.so
复制到目录
/usr/lib
中,再试
试。
# mv libmyhello.so /usr/lib
# ./hello
Hello everyone!
#
成功了。这也进一步说明了动态库在程序运行时是需要的。
我们回过头看看,发现使用静态库和使用动态库编译成目标程序使用的
gcc
命令完全一样,
那当静态库和动态库同名时,
gcc
命令会使用哪个库文件呢
?抱着对问题必究到底的心情,
来试试看。
先删除除
.c
和
.h
外的所有文件,恢复成我们刚刚编辑完举例程序状态。
# rm -f hello hello.o /usr/lib/libmyhello.so
# ls
hello.c hello.h main.c
#
再来创建静态库文件
libmyhello.a
和动态库文件
libmyhello.so
。
# gcc -c hello.c
# ar -cr libmyhello.a hello.o
(或
-cvr
)
# gcc -shared -fPIC -o libmyhello.so hello.o
# ls
hello.c hello.h hello.o libmyhello.a libmyhello.so main.c
#
通过上述最后一条
ls
命令,可以发现静态库文件
libmyhello.a
和动态库文件
libmyhello.s
o
都已经生成,并都在当前目录中。然后,我们运行
gcc
命令来使用函数库
myhello
生成目
标文件
hello
,并运行程序
hello
。
# gcc -o hello main.c -L. –lmyhello
(动态库和静态库同时存在时,优先使用动态库,当然,如果直接
#gcc main.c libmyhello.a -o hello
的话,就是指定为静态库了)
# ./hello
./hello: error while loading shared libraries: libmyhello.so: cannot open shar
ed object file: No such file or directory
#
从程序
hello
运行的结果中很容易知道,当静态库和动态库同名时,
gcc
命令将优先使用动
态库,默认去连
/usr/lib
和
/lib
等目录中的动态库,将文件
libmyhello.so
复制到目录
/usr/lib
中即可。
以下为所有运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/b0180fccf909c4ff8fdba26dd47136a3.png)
在第一次作业的程序代码基础进行改编,除了x2x函数之外,再扩展写一个x2y函数(功能自定),main函数代码将调用x2x和x2y ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。
扩写x2y函数
![](https://i-blog.csdnimg.cn/blog_migrate/6344d0c2b8a6b84ced9870a69fa309bc.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e55abc8f74e7426acd918a271e65240e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0464d5b2ec583f64bd2e89e66fc71a8a.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c84c6380518bb5929a583ea89742f6c0.png)
将3个 .c文件编译为 .o文件
![](https://i-blog.csdnimg.cn/blog_migrate/4f1c7e63f01544a54aa44e6f684a8137.png)
将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件
![](https://i-blog.csdnimg.cn/blog_migrate/ba78288474053396d74ef45048611313.png)
然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序
先生成
main.o
:
gcc -c main.c
再生成可执行文件:
gcc -o sub main.o libmysub.a
![](https://i-blog.csdnimg.cn/blog_migrate/c871b68fa9cf2fd32307dd91472e4f16.png)
再来看可执行文件sub的大小
![](https://i-blog.csdnimg.cn/blog_migrate/bd310ec62ff6c27ec043cfb5c63bab95.png)
可以看见sub的大小为8.4KB。
将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序
先生成.so动态文件库,如图所示
![](https://i-blog.csdnimg.cn/blog_migrate/a72058bee95dd04aaa0446abe6ae58cd.png)
再连接文件库,生成可执行程序
![](https://i-blog.csdnimg.cn/blog_migrate/50cbf31d8ab7598c7c727974ab42fd97.png)
二、Gcc不是一个人在战斗。请说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍
GCC(GNU Compiler Collection)是一套开源的编译器工具集,它由多个组件构成,每个组件都有不同的用途。下面是GCC中几个常见组件的用途说明:
-
GNU C编译器(GCC):GCC最初是为C语言编写的编译器,但后来它成为了支持其他编程语言的广泛编译器。它是GCC工具集的核心组件,可将C、C++、Objective-C和Objective-C++等高级语言源代码翻译成可执行的机器代码。
-
GNU C++编译器(G++):G++是GCC的C++编译器,可将C++语言源代码编译成可执行的机器代码。它是在GCC的基础上针对C++语言进行了扩展和改进。
-
GNU Fortran编译器(GFortran):GFortran是GCC的Fortran编译器,用于将Fortran语言源代码编译成可执行的机器代码。它支持多个Fortran标准,并具有丰富的优化选项。
-
GNU Ada编译器(GNAT):GNAT是GCC的Ada编译器,用于将Ada语言源代码编译成可执行的机器代码。它支持多个Ada标准,并提供了广泛的编译选项。
-
GNU Objective-C编译器(GCC-ObjC):GCC-ObjC是GCC的Objective-C编译器,用于将Objective-C语言源代码编译成可执行的机器代码。它是在GCC的基础上为Objective-C语言进行了扩展和改进。
除了以上几个组件外,GCC还包含其他工具和库,如GNU汇编器(AS)、GNU链接器(LD)、GNU调试器(GDB)等,它们在编译和调试过程中发挥重要作用。总的来说,GCC是一套功能强大的编译器工具集,可用于编译和构建各种编程语言的源代码,并生成可执行的机器代码。
三. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。下图是一个Cortex-M4的存储器地址映射示意图(与Cortex-M3/stm32F10x基本相同,只存在微小差异)
程序示例:
![](https://i-blog.csdnimg.cn/blog_migrate/2203a6c3204cbf454a4c5c16817e9a9f.png)
执行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/dde416befba2fb699b67e220b1923a5f.png)
归纳与总结:
对于Ubuntu(x86)系统:
- 全局常量和全局变量通常存储在数据段或BSS段,其地址在程序运行时是固定的。
- 局部变量和静态变量存储在栈中,其地址在程序运行时动态分配。
- 动态分配的内存(堆)使用malloc函数,在堆区分配内存,其地址也是在程序运行时动态确定的。
对于STM32(Keil):
- 全局常量和全局变量存储在Flash存储器中,其地址在编译时确定并存储在Flash中。
- 局部变量和静态变量存储在栈中,其地址在程序运行时动态分配。
- 动态分配的内存(堆)使用malloc函数,在SRAM或SDRAM区分配内存,其地址也是在程序运行时动态确定的。
通过上述程序,你可以在Ubuntu上使用printf输出变量的地址,而在STM32上,你可以使用串口printf将变量的地址打印到上位机串口助手上。这样就可以进行对比分析,加深对于堆、栈、全局和局部变量在不同平台下的分配地址的理解。
至于ARM Cortex-M / STM32F10x的存储器地址映射,它将不同类型的存储器(如Flash、RAM等)划分到不同的地址空间。通过正确理解存储器地址映射,我们可以正确分配和访问变量,并与相应的寄存器进行交互,以实现所需的功能。对于具体的地址映射细节,你可以参考芯片的数据手册来了解。