GCC背后的故事&C程序常量变量的地址分配

文章目录


前言

在当今的计算机世界中,GCC(GNU Compiler Collection)已经成为了一种不可或缺的存在。作为一款开源的编译器,GCC为各种不同的编程语言提供了强大的编译能力,其中最为核心的就是对C语言的支持。而在C语言的编译过程中,常量变量地址的分配是一个十分重要且有趣的环节。这篇博客将带您了解GCC背后的故事,并探讨C程序中常量变量的地址分配。

GCC的历史可以追溯到1980年代,当时计算机行业正处于巨变之中。自由软件和开源文化的兴起,使得一批优秀的程序员开始着手开发一个自由的编译器。这个想法最初由Richard Stallman提出,并得到了许多志愿者的支持。1989年,GCC正式发布,成为了自由软件运动的一个重要里程碑。

随着时间的推移,GCC逐渐成为了开源编译器的代表,它支持的编程语言也越来越多。通过不断的改进和优化,GCC的编译速度和代码生成质量都得到了极大的提升。如今,GCC已经成为了许多操作系统和嵌入式系统的主要编译器,同时也是许多大型项目的重要工具。

一、阅读、理解和学习材料“用gcc生成静态库和动态库.pdf”和“静态库.a与.so库文件的生成与使用.pdf”,请在Linux系统(Ubuntu)下如实仿做一遍。

1、用 gcc 生成 .a 静态库和 .so 动态库

我们通常把一些公用函数制作成函数库,供其它程序使用。函数库分为静态和动态库两种。
静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程
序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需
要动态库存在。
本文主要通过举例来说明在 Linux 中如何创建静态库和动态库,以及使用它们。
在创建函数库前,我们先来准备举例用的源程序,并将函数库的源程序编译成.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 步:将 hello.c 编译成.o 文件。
无论静态库,还是动态库,都是由.o 文件创建的。因此,我们必须将源程序 hello.c 通过 g
cc 先编译成.o 文件。在系统提示符下键入以下命令得到 hello.o 文件。
#gcc -c hello.c

我们运行 ls 命令看看是否生存了 hello.o 文件。
ls

在这里插入图片描述
在 ls 命令结果中,我们看到了 hello.o 文件,本步操作完成。
下面我们先来看看如何创建静态库,以及使用它。
第 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 步:在程序中使用静态库。
静态库制作完了,如何使用它内部的函数呢?只需要在使用到这些公用函数的源程序中包
含这些公用函数的原型声明,然后在用 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 步:由.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 步:在程序中使用动态库;
在程序中使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含
这些公用函数的原型声明,然后在用 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 命令完全一样
在这里插入图片描述

2、Linux 下静态库.a 与.so 库文件的生成与使用

先创建一个作业目录,保存本次练习的文件。
#mkdir test2
#cd test2
在这里插入图片描述

然后用 vim、nano 或 gedit 等文本编辑器编辑生成所需要的四个文件 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、生成目标文件(xxx.o)
gcc -c A1.c A2.c
在这里插入图片描述

1.2、生成静态库.a 文件
ar crv libafile.a A1.o A2.o
在这里插入图片描述

1.3、使用.a 库文件,创建可执行程序(若采用此种方式,需保证生成的.a 文件与.c 文件保
存在同一目录下,即都在当前目录下)
gcc -o test test.c libafile.a
./test
在这里插入图片描述

3、共享库.so 文件的生成与使用

2.1、生成目标文件(xxx.o)(此处生成.o 文件必须添加"-fpic"(小模式,代码少),否则在生成.so
文件时会出错)
gcc -c -fpic A1.c A2.c
2.2、生成共享库.so 文件
gcc -shared *.o -o libsofile.so
2.3、使用.so 库文件,创建可执行程序
gcc -o test test.c libsofile.so
./test
发现出现错误:
./test: error while loading shared libraries: libsofile.so: cannot open shared object file: No
such file or directory
在这里插入图片描述

运行 ldd test,查看链接情况
ldd test
linux-vdso.so.1 => (0x00007fff0fd95000)
libsofile.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f937b5de000)
/lib64/ld-linux-x86-64.so.2 (0x0000563f7028c000)
发现确实是找不到对应的.so 文件。
这是由于 linux 自身系统设定的相应的设置的原因,即其只在/lib and /usr/lib 下搜索对应
的.so 文件,故需将对应 so 文件拷贝到对应路径。
sudo cp libsofile.so /usr/lib
再次执行./test,即可成功运行。
./test
在这里插入图片描述

同时可直接使用 gcc -o test test.c -L. -lname,来使用相应库文件
其中,
-L.:表示在当前目录下,可自行定义路径 path,即使用-Lpath 即可。
-lname:name:即对应库文件的名字(除开 lib),即若使用 libafile.a,则 name 为 afile;
若要使用 libsofile.so,则 name 为 sofile)。

二、在第一次作业的程序代码基础进行改编,除了x2x函数之外,再扩展写一个x2y函数(功能自定),main函数代码将调用x2x和x2y ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。

代码:

sub1.c

  float x2x(int m,int n)
    {
       float b;
       b = m * n;
       return b;
    }

sub2.c

  float x2y(int x, int y)
    { 
        float z;
        z=x+y;
        return z;
    }

sub.h

    #ifndef SUB_H
    #define SUB_H
    float x2x(int m,int n);
    float x2y(int x,int y);
    #endif

main.c

#include <stdio.h> 
#include "sub.h" 
int main()
{ 
   int m=8,n=6;
   float x,y;
   x = x2x(m,n);
   y = x2y(m,n);
   printf("%f\r\n", x);
   printf("%f\r\n", y);
   return 0;
}

(2)将.c文件转成.o文件
在这里插入图片描述

(3)静态库

将两个目标文件用ar工具生成.a静态库

输入gcc命令
在这里插入图片描述

(4)动态库

将两个目标文件用ar工具生成.so动态库,并用gcc链接。
在这里插入图片描述

(5)静态库和动态库大小对比
输入命令:ll

在这里插入图片描述

三. Gcc请说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:

阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍。

1.GCC(GNU Compiler Collection)是一套广泛使用的编译工具集,用于编译各种编程语言的源代码。它包括多个不同的组件,每个组件都有不同的用途。以下是GCC编译工具集中一些常见组件的用途:

GCC编译器(gcc):GCC编译器是GNU编译工具集的核心组件之一。它用于将高级编程语言(如C、C++、Fortran等)的源代码编译成可执行程序或库。GCC编译器支持多种目标架构和操作系统,因此可以用于跨平台开发。

G++编译器(g++):G++是GCC编译器的C++版本,用于编译C++源代码。它支持C++的所有功能,包括面向对象编程和STL(标准模板库)等。

GCC汇编器(as):GCC汇编器用于将汇编语言源代码汇编成目标机器代码。它是编写底层系统软件或嵌入式编程的重要工具。

GNU链接器(ld):GNU链接器用于将编译后的目标文件链接在一起,创建可执行程序或共享库。它还负责解析符号、解决符号引用以及生成最终的可执行文件。

GNU调试器(gdb):GDB是一个功能强大的调试器,用于在程序运行时进行调试。它允许开发人员检查变量的值、跟踪程序的执行流程、设置断点以及分析程序崩溃的原因。

GNU二进制工具(binutils):Binutils包括一组二进制工具,如objdump(用于反汇编目标文件)、objcopy(用于复制目标文件)、readelf(用于查看ELF文件的信息)等。这些工具在处理二进制文件时非常有用。

2.ELF文件

ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。1
ELF文件格式如下图,位于ELF Header和Section Header Table 之间的都是段(Section)。一个典型的ELF文件包含下面几个段:
.text:已编译程序的指令代码段。
.rodata:ro 代表 read only,即只读数据(譬如常数 const)。
.data:已初始化的 C 程序全局变量和静态局部变量。
.bss:未初始化的 C 程序全局变量和静态局部变量。
.debug:调试符号表,调试器用此段的信息帮助调试。
在这里插入图片描述一个典型的 ELF 文件包含下面几个段:
.text:已编译程序的指令代码段。
.rodata:ro 代表 read only,即只读数据(譬如常数 const)。
.data:已初始化的 C 程序全局变量和静态局部变量。
.bss:未初始化的 C 程序全局变量和静态局部变量。
.debug:调试符号表,调试器用此段的信息帮助调试。

四. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。下图是一个Cortex-M4的存储器地址映射示意图(与Cortex-M3/stm32F10x基本相同,只存在微小差异)

在这里插入图片描述

1、在Ubuntu(x86)系统中的C程序:

堆(Heap):在Ubuntu系统中,堆是由C标准库的malloc和free函数管理的动态内存分配区域。堆的地址通常较高,可以随着程序运行动态增长和收缩。您可以使用malloc来分配堆上的内存。

栈(Stack):栈是用于函数调用和局部变量的存储的地方。在Ubuntu系统中,栈通常位于程序的低地址端,随着函数的调用和返回而增长和缩小。局部变量通常存储在栈上。

全局变量(Global Variables):全局变量是定义在函数之外的变量,它们在程序的整个生命周期内存在,并存储在数据段中。全局变量的地址通常在可执行文件加载时确定。

局部变量(Local Variables):局部变量是定义在函数内部的变量,它们在函数调用时创建,当函数返回时销毁,通常存储在栈上。

静态变量(Static Variables):静态变量是使用static关键字定义的变量,它们的生命周期跨越函数调用,通常存储在数据段中。静态变量的地址在可执行文件加载时确定。

2、在STM32(Keil)中的C程序:

堆(Heap):在嵌入式系统中,堆的管理通常需要自己实现或依赖RTOS(Real-Time Operating System)。您可以使用STM32标准库或RTOS提供的内存分配函数来管理堆上的内存。

栈(Stack):栈在嵌入式系统中的概念与Ubuntu系统类似,用于函数调用和局部变量的存储。栈的大小和位置通常由链接脚本和编译器配置。

全局变量(Global Variables):全局变量在嵌入式系统中与桌面系统相似,但它们的地址映射到MCU的内存地址空间中。

局部变量(Local Variables):局部变量在嵌入式系统中与桌面系统相似,通常存储在栈上。

静态变量(Static Variables):静态变量在嵌入式系统中也与桌面系统相似,通常存储在数据段中。

了解ARM Cortex-M系列处理器以及STM32F10x系列微控制器的存储器地址映射是嵌入式系统开发的关键部分。这些概念有助于您在开发STM32微控制器的应用程序时有效地管理内存。下面是一个简要的关于ARM Cortex-M存储器地址映射的概述,以及STM32F10x系列的一些常见特点:
ARM Cortex-M存储器地址映射:

ARM Cortex-M系列处理器采用了一种统一的存储器地址映射方案,其中包括以下几个主要区域:

Code区域(Flash存储器):这是用于存储程序代码的地方。程序代码通常位于Flash存储器中,并且在程序运行时不能被修改。程序通常从Flash中加载到RAM中执行,因此在存储器地址映射中有两个主要部分:Flash存储器区域和RAM区域。

RAM区域:RAM区域用于存储程序的数据和堆栈。它可以分为不同的部分,包括静态RAM(SRAM)、堆栈内存等。SRAM通常用于存储全局变量、静态变量以及堆和栈的分配。

外设寄存器区域:这些区域包含了与微控制器外设(如GPIO、UART、SPI等)相关的寄存器。程序通过访问这些寄存器来配置和控制外设。

系统控制区域:此区域包含与系统控制和调试相关的寄存器,例如中断控制器、系统控制寄存器等。

Boot ROM区域:某些Cortex-M微控制器具有用于引导和启动设备的Boot ROM。

STM32F10x系列的存储器地址映射:

STM32F10x系列微控制器是基于ARM Cortex-M3核心的,具有自己的存储器地址映射特点:

Flash存储器:STM32F10x系列通常具有内置的Flash存储器,用于存储程序代码。Flash的大小和类型取决于具体的型号,可以从数KB到数百KB不等。

SRAM:这些微控制器还包括SRAM用于存储数据和堆栈。SRAM的大小也因型号而异。

外设寄存器:STM32F10x系列的外设寄存器位于特定的存储器地址范围内,用于配置和控制各种外设。每个外设都有一组相关的寄存器,您可以通过读写这些寄存器来与外设进行通信。

系统控制区域:这个区域包含与系统控制和中断控制相关的寄存器。

总结

在当今的计算机世界中,GCC(GNU Compiler Collection)已经成为了一种不可或缺的存在。作为一款开源的编译器,GCC为各种不同的编程语言提供了强大的编译能力,其中最为核心的就是对C语言的支持。而在C语言的编译过程中,常量变量地址的分配是一个十分重要且有趣的环节。这篇博客将带您了解GCC背后的故事,并探讨C程序中常量变量的地址分配。

GCC的历史可以追溯到1980年代,当时计算机行业正处于巨变之中。自由软件和开源文化的兴起,使得一批优秀的程序员开始着手开发一个自由的编译器。这个想法最初由Richard Stallman提出,并得到了许多志愿者的支持。1989年,GCC正式发布,成为了自由软件运动的一个重要里程碑。

随着时间的推移,GCC逐渐成为了开源编译器的代表,它支持的编程语言也越来越多。通过不断的改进和优化,GCC的编译速度和代码生成质量都得到了极大的提升。如今,GCC已经成为了许多操作系统和嵌入式系统的主要编译器,同时也是许多大型项目的重要工具。

参考:https://blog.csdn.net/zxp_124/article/details/120728073

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值