前言

在嵌入式与 Linux 开发领域,编译链接逻辑、库文件应用及内存变量分配是核心基础能力。本文聚焦三大关键模块:其一在 Ubuntu 22.04上实操 GCC 编译,基于改编的 C 程序生成静态库与动态库,对比可执行文件大小以掌握库的构建与使用;其二学习 GCC 工具集各组件用途,理解 ELF 文件格式;其三编写 C 程序,分别在Ubuntu 与 STM32(Keil)环境验证变量存储,对比分析堆、栈、全局 / 局部变量地址差异,深化 ARM Cortex-M 存储器映射认知。本文作为实验博客,将梳理实验过程、呈现关键结果,为后续开发夯实基础。


一、gcc生成静态库和动态库

在模块化开发中,当多个程序需要复用相同功能时,“库” 是高效的解决方案。库本质是一组编译后的目标文件(.o)的集合,分为两类:

  • 静态库(.a):
    编译链接时,编译器会将库中被调用的代码 “复制” 到可执行文件中。程序运行时不依赖外部库,但可执行文件体积较大,且库更新后需重新编译程序。
  • 动态库(.so):
  • 编译链接时仅记录库的引用关系,程序运行时才加载库文件。可执行文件体积小,库更新后无需重新编译程序,但运行时需确保库文件存在且路径正确。
  • 两者区别:前者是编译连接的,后者是程序运行载入的。

在创建函数库前,我们先来准备举例用的源程序,并将函数库的源程序编译成.o 文件。

1. 编辑生成例子程序 hello.h、hello.c 和 main.c

建立工作环境:在Ubuntu 22.04系统上打开终端,建立test1文件夹,并转到该路径下。
在这里插入图片描述
然后用 vim、nano 或 gedit 等文本编辑器编辑生成所需要的 3 个文件,我这里用的是vim编辑。
在这里插入图片描述
具体代码:

程序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;
}

hello.c是函数库的源程序,其中包含公用函数 hello,该函数将在屏幕上输出"HelloXXX!"。hello.h为该函数库的头文件。main.c为测试库文件的主程序,在主程序中调用了公用函数 hello()。

2. gcc编译得到.o文件

无论静态库,还是动态库,都是由.o 文件创建的。因此,我们必须将源程序 hello.c 通过 gcc 先编译成.o 文件。在系统提示符下键入以下命令得到 hello.o 文件。

gcc -c hello.c

在这里插入图片描述
llls 命令结果中,我们看到了 hello.o 文件,本步操作完成。

3. 建立静态库

ar -crv libHello.a hello.o

在这里插入图片描述

4. 使用静态库

在用 gcc 命令生成目标文件时指明静态库名,gcc 将会从静态库中将公用函数连接到目标文件中。注意,gcc 会在静态库名前加上前缀 lib,然后追加扩展名.a 得到的静态库文件名来查找静态库文件。

下面先生成目标程序 hello,然后运行 hello 程序看看结果如何。

gcc main.c libHello.a -o hello
./hello

执行后有以下结果:
Hello everyone!
在这里插入图片描述
程序照常运行,静态库中的公用函数已经连接到目标文件中了。
我们继续看看如何在 Linux 中创建动态库。我们还是从.o 文件开始。

5. 建立动态库

动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀 lib,但其文件扩展名为.so。例如:我们将创建的动态库名为 Hello,则动态库文件名就是 libHello.so
用 gcc 来创建动态库:

gcc -shared -fPIC -o libHello.so hello.o

6. 使用动态库

在程序中使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明动态库名进行编译。

gcc main.c libHello.so -o hello

在这里插入图片描述

./hello

在这里插入图片描述发现出错了,不要慌,我们看看错误提示,原来是找不到动态库文件 libmyhello.so。程序在运行时,会在/usr/lib 和/lib 等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。我们将文件 libmyhello.so 复制到目录/usr/lib 中,再试试。

mv libHello.so /usr/lib
./hello

输出Hello everyone!,成功执行!这也进一步说明了动态库在程序运行时是需要的。

7. 同时存在静态库和动态库

我们回过头看看,发现使用静态库和使用动态库编译成目标程序使用的 gcc 命令完全一样,那当静态库和动态库同名时,gcc 命令会使用哪个库文件呢?

先删除除.c 和.h 外的所有文件,恢复成我们刚刚编辑完举例程序状态。

rm -f hello hello.o /usr/lib/libHello.so
ls
hello.c hello.h main.c

再来创建静态库文件 libHello.a 和动态库文件 libHello.so

gcc -c hello.c
ar -cr libHello.a hello.o (或-cvr )
gcc -shared -fPIC -o libHello.so hello.o
ls
hello.c hello.h hello.o libHello.a libHello.so main.c

通过上述最后一条 ls 命令,可以发现静态库文件 libmyhello.a 和动态库文件 libmyhello.so 都已经生成,并都在当前目录中。然后,我们运行 gcc 命令来使用函数库 myhello 生成目标文件 hello,并运行程序 hello。

gcc -o hello main.c -L. –lHello
./hello
: error while loading shared libraries: libmyhello.so: cannot open shared object file: No such file or directory

动态库和静态库同时存在时,优先使用动态库。
从以上实验可知:当静态库和动态库同名时,gcc 命令将优先使用动态库,默认去连/usr/lib /lib 等目录中的动态库,将文件libHello.so复制到目录/usr/lib中即可。


二、实例2

我们刚刚通过一个简单的实例1初步认识了静态库和动态库,接下来我们将通过实例2来加深理解。

1.具体代码

sub1.c

#include "sub.h"
float x2x(int a,int b)
{
	float m=(float)a;
	float n=(float)b;
	return m/n;
}

sub2.c

#include "sub.h"
float x2y(int a,int b)
{
	float c=0;
	c=a+b;
	return c;
}

sub.h

#ifndef SUB_H
#define SUB_H
float x2x(int a,int b);
float x2y(int a,int b);
#endif

main.c

#include<stdio.h>
#include"sub.h"
void main()
{
	int a,b;
	printf("Please input the value of a:");
	scanf("%d",&a);
	printf("Please input the value of b:");
	scanf("%d",&b);
	printf("a+b=%.2f\n",x2y(a,b));
	printf("a/b=%.2f\n",x2x(a,b));
}

在这里插入图片描述
gcc -c sub1.c sub2.c

2.静态库

ar crv libsub.a sub1.o sub2.o
gcc -o main main.c libsub.a

3.动态库

gcc -shared -fPIC libsub.so sub1.o sub2.o
gcc -o main main.c libsub.so
别忘了把 libsub.so放在/usr/lb
sudo cp libsub.so /usr/lib
在这里插入图片描述

4.进一步比较静态库与动态库

在这里插入图片描述
在这里插入图片描述
通过比较,动态库链接的可执行文件体积更小(因未包含库代码),但依赖外部.so 文件;静态库链接的文件独立运行,但体积更大。


三、gcc编译工具及其用途

GCC并非单一工具,而是由多个组件协作完成编译流程:

  • gcc:编译器前端,负责预处理、编译(生成汇编代码);
  • as:汇编器,将汇编代码转为目标文件(.o);
  • ld:链接器,将目标文件与库链接为可执行文件;
  • ar:归档工具,用于创建静态库(.a);
  • objdump:查看目标文件 / 可执行文件的信息(如符号表、汇编代码)。

1.准备工作

创建一 个工作目录 test0,然后用文本编辑器生成一个 C 语言编写的简单 Hello.c 程序为示例,其源代码如下所示:

#include <stdio.h>
//此程序很简单,仅仅打印一个 Hello World 的字符串。
int main(void)
{
printf("Hello World! \n");
return 0;
}

2.编译过程

2.1 预处理

使用 gcc 进行预处理的命令如下:
gcc -E hello.c -o hello.i
// 将源文件 hello.c 文件预处理生成 hello.i
// GCC 的选项-E 使 GCC 在进行完预处理后即停止

2.2 编译

编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。
使用 gcc 进行编译的命令如下:
gcc -S hello.i -o hello.s
// 将预处理生成的 hello.i 文件编译生成汇编程序 hello.s
// GCC 的选项-S 使 GCC 在执行完编译后停止,生成汇编程序

2.3 汇编

当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o 目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。

使用 gcc 进行汇编的命令如下:
gcc -c hello.s -o hello.o
// 将编译生成的 hello.s 文件汇编生成目标文件 hello.o
// GCC 的选项-c 使 GCC 在执行完汇编后停止,生成目标文件

2.4 链接

链接也分为静态链接和动态链接
如果使用命令“gcc hello.c -o hello”则会使用动态库进行链接,生成的ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的动态库(使用 Binutils 的 ldd 命令查看)如下所示:
在这里插入图片描述
ldd hello //可以看出该可执行文件链接了很多其他动态库,主要是 Linux 的 glibc 动态库
在这里插入图片描述
如 果 使 用 命 令 “ gcc -static hello.c -o hello”则 会 使 用 静 态 库 进 行 链 接 ,生成的 ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的动态库(使用 Binutils 的 ldd 命令查看)如下所示:

在这里插入图片描述
在这里插入图片描述

3. 分析 ELF 文件

3.1 ELF 文件的段

Linux 下的目标文件、可执行文件、库文件均采用 ELF(Executable and Linkable Format)格式,包含:

  • 文件头:记录架构、类型(可执行 / 库)、入口地址等;
  • 节(Section):如.text(代码)、.data(初始化数据)、.bss(未初始化数据);
  • 符号表:记录变量 / 函数的名称与地址映射。

3.2 反汇编 ELF

由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法。

使用 objdump -D 对其进行反汇编如下:
objdump -D hello
在这里插入图片描述


四、Ubuntu(x86)系统和STM32(Keil)中编程验证

1. Ubuntu(x86)系统

1.1 代码撰写

#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.2 编译运行代码

在这里插入图片描述
在这里插入图片描述

2. STM32(Keil)

keil 环境下默认的内存配置说明
在这里插入图片描述

① 默认分配的ROM区域是0x8000000开始,大小是0x10000的一片区域,那么这篇区域是只读区域,不可修改,也就是存放的代码区和常量区

② 默认分配的RAM区域是0x20000000开始,大小是0x5000的一片区域,这篇区域是可读写区域,存放的是静态区、栈区和堆区。

2.1 变量/常量定义说明

为验证不同类型变量的存储位置,定义以下变量 / 常量,涵盖全局与局部场景:

  • 全局定义
    +变量 global_temp
    +静态变量 global_temp_static
    +常量 global_const
    +静态常量 global_const_static
  • 局部定义
    +变量 local_temp
    +静态变量 local_temp_static
    +常量 local_const
    +静态常量 local_const_static

2.2 实现方案

  • 步骤1:按上述说明写函数,定义变量和函数,代码如下:
int global_temp;
static int global_temp_static;
const int global_const = 1;
static const int global_const_static = 2;

/*以下变量用于存储变量的地址*/
int global_temp_adress;
int global_temp_static_adress;
int global_const_adress;
int global_const_static_adress;
int local_temp_adress;
int local_temp_static_adress;
int local_const_adress;
int local_const_static_adress;

int main(void)
{	
	int local_temp;
	static int local_temp_static;
	const int local_const = 3;
	static const int local_const_static = 4;
	global_temp_adress 				= (int)&global_temp;
	global_temp_static_adress 		= (int)&global_temp_static;
	global_const_adress 			= (int)&global_const;
	global_const_static_adress 		= (int)&global_const_static;
	local_temp_adress 				= (int)&local_temp;
	local_temp_static_adress 		= (int)&local_temp_static;
	local_const_adress				= (int)&local_const;
	local_const_static_adress		= (int)&local_const_static;
	while(1);
}

  • 步骤2:配置仿真
    配置仿真的参数,以STM32F103C8T6为例:
    在这里插入图片描述
  • 步骤3:进入仿真,通过Watch窗口查看变量的值以查看变量地址
    将各个变量添加到Watch窗口:
    在这里插入图片描述

2.3 变量/常量位置分析

STM32 的存储器分为 RAM(随机存取存储器,用于临时数据)和 Flash(闪存,用于存储代码和常量),结合观测结果,各类变量的存储位置规律如下:

  1. 存储器空间分布基础
  • RAM(地址范围:0x20000000 起):用于存储可修改的变量,按功能分为:
    数据段:存储全局变量、静态变量(编译时确定大小和起始地址);
    栈空间:存储局部变量(大小由.s文件中Stack_Size定义,从高地址向低地址生长);
    堆空间:用于动态内存分配(如malloc,起始地址接栈空间结束地址,从低地址向高地址生长)。
  • Flash(地址范围:0x08000000 起):用于存储不可修改的内容,包括:
    代码段:存储程序指令;
    常量段:存储全局常量、静态常量等只读数据。
  1. 变量 / 常量的具体存储位置
    结合观测地址,各类变量的存储位置对号入座如下:
  • RAM 空间
    • 数据段(0x20000000 – 0x20000030):
      • 全局普通变量 global_temp → 0x20000000
      • 全局静态变量 global_temp_static → 0x20000004
      • 局部静态变量 local_temp_static → 0x20000028
    • 栈空间(0x20000030 – 0x20000430,Stack_Size=0x400):
      • 局部普通变量 local_temp → 0x20000428
      • 局部常量 local_const → 0x20000424
  • Flash 空间(常量段)
    • 全局常量 global_const → 0x08000258
    • 全局静态常量 global_const_static → 0x0800025C
    • 局部静态常量 local_const_static → 0x08000260
  1. 规律总结
  • 全局 / 局部静态变量(无论是否初始化)均存储在 RAM 的数据段,生命周期与程序一致;
  • 局部普通变量、局部常量存储在 RAM 的栈空间,生命周期随函数调用结束而释放;
  • 全局常量、静态常量(无论全局 / 局部)均存储在 Flash 的常量段,不可修改且生命周期与程序一致。

总结

本次实验围绕嵌入式开发核心基础展开,通过实操与对比,系统掌握了GCC库文件构建、工具链工作原理及跨平台变量存储规律,为后续嵌入式开发夯实了理论与实践基础。

在库文件构建模块,通过两次实例验证了静态库(.a)与动态库(.so)的差异:静态库需将代码复制到可执行文件,生成的程序体积更大但可独立运行,而动态库仅记录引用关系,程序体积小却依赖外部库文件,且明确了GCC在两者同名时优先调用动态库的特性,为实际开发中库的选择提供了依据。

GCC工具链部分,拆解了“预处理-编译-汇编-链接”四步流程,明晰了gcc(前端编译)、as(汇编)、ld(链接)等组件的分工,同时通过分析ELF文件格式及静态/动态链接的可执行文件大小差异,深入理解了程序从源码到可执行文件的转化逻辑。

跨平台变量存储验证是本次实验的重点,对比Ubuntu(x86)与STM32(ARM Cortex-M)可知:虽两者均遵循“全局/静态变量存数据段、局部变量存栈、常量存只读区”的规律,但存储器映射存在显著差异——STM32的Flash(0x08000000起)与RAM(0x20000000起)地址固定,栈从高地址向低地址生长;而Ubuntu的堆栈生长方向相反,地址分配更灵活。

综上,本次实验不仅掌握了具体的开发工具与操作流程,更建立了“代码-编译-存储-运行”的完整认知,为后续模块化开发、程序调试及硬件适配提供了关键支撑。


参考

  1. https://blog.csdn.net/qq_39257301/article/details/88691913
  2. https://blog.csdn.net/qq_46467126/article/details/121875496
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐