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

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

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

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

先创建一个作业目录,保存本次练习的文件。
#mkdir test1
#cd test1
然后用 vim、nano 或 gedit 等文本编辑器编辑生成所需要的 3 个文件。

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

第 2 步:将 hello.c 编译成.o 文件。

在系统提示符下键入以下命令得到 hello.o 文件。

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

ls

hello.c hello.h hello.o main.c
在 ls 命令结果中,我们看到了 hello.o 文件,本步操作完成。

第 3 步:由.o 文件创建静态库。

创建静态库用 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 -o hello main.c -L. –lmyhello

方法二:
#gcc main.c libmyhello.a -o hello

第 5 步:由.o 文件创建动态库文件。

在系统提示符下键入以下命令得到动态库文件 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 -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!
成功了。这也进一步说明了动态库在程序运行时是需要的。
在这里插入图片描述
在这里插入图片描述

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

先创建一个作业目录,保存本次练习的文件。
#mkdir test2
#cd test2
然后用 vim、nano 或 gedit 等文本编辑器编辑生成所需要的四个文件 A1.c 、 A2.c、 A.h、 test.c
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

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

2、 共享库.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)。

在这里插入图片描述

三、GCC编译器背后的故事

(一)准备工作

由于 GCC 工具链主要是在 Linux 环境中进行使用,因此本文也将以 Linux 系统作为工作环境。为了能够演示编译的整个过程,先创建一个工作目录 test0,然后用文本编辑器生成一个 C 语言编写的简单 Hello.c 程序为示例

在这里插入图片描述

(二) 编译过程
1.预处理预处理的过程主要包括以下过程:

(1)将所有的#define 删除,并且展开所有的宏定义,并且处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif 等。
(2)处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
(3)删除所有注释“//”和“/* */”。
(4)添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
(5) 保留所有的#pragma 编译器指令,后续编译过程需要使用它们。
使用 gcc 进行预处理的命令如下:
$ gcc -E hello.c -o hello.i
// 将源文件hello.c文件预处理生成hello.i
// GCC的选项-E使GCC在进行完预处理后即停止

2.编译

编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。使用 gcc 进行编译的命令如下:
$ gcc -S hello.i -o hello.s
// 将预处理生成的hello.i文件编译生成汇编程序hello.s // GCC的选项-S使GCC在执行完编译后停止,生成汇编程序上述命令生成的汇编程序 hello.s 的代码片段如下所示,其全部为汇编代码。 // hello.s代码片段
main:
.LFB0:
.cfi_startproc pushq %rbp
.cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp
.cfi_def_cfa_register 6 movl $.LC0, %edi call puts movl $0, %eax popq %rbp
.cfi_def_cfa 7, 8 ret
.cfi_endproc

3.汇编

汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o 的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用 Binutils 中的汇编器 as 根据汇编指令和处理器指令的对照表一一翻译即可。
当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o 目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。使用 gcc 进行汇编的命令如下:
$ gcc -c hello.s -o hello.o
// 将编译生成的hello.s文件汇编生成目标文件hello.o
// GCC的选项-c使GCC在执行完汇编后停止,生成目标文件
//或者直接调用as进行汇编
$ as -c hello.s -o hello.o //使用Binutils中的as将hello.s文件汇编生成目标文件注意:hello.o 目标文件为 ELF(Executable and Linkable Format)格式的可重定向文件。

在这里插入图片描述

四、Linux GCC 常用命令

简单编译

示例程序如下:

在这里插入图片描述

这个程序,一步到位的编译指令是:
gcc test.c -o test

1.预处理

gcc -E test.c -o test.i 或 gcc -E test.c
可以输出 test.i 文件中存放着 test.c 经预处理之后的代码。打开 test.i 文件,看一看,就明白了。后面那条指令,是直接在命令行窗口中输出预处理后的代码.
gcc 的-E 选项,可以让编译器在预处理后停止,并输出预处理结果。在本例中,预处理结果就是将 stdio.h 文件中的内容插入到 test.c 中了。

2.编译为汇编代码(Compilation)

预处理之后,可直接对生成的 test.i 文件编译,生成汇编代码:
gcc -S test.i -o test.s
gcc 的-S 选项,表示在程序编译期间,在生成汇编代码后,停止,-o 输出汇编代码文件。

3.汇编(Assembly)

对于上一小节中生成的汇编代码文件 test.s,gas 汇编器负责将其编译为目标文件,如下:
gcc -c test.s -o test.o

4.连接(Linking)

gcc 连接器是 gas 提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。
对于上一小节中生成的 test.o,将其与C标准输入输出库进行连接,最终生成程序 test
gcc test.o -o test
在命令行窗口中,执行./test, 让它说 HelloWorld 吧!

5.多个程序文件的编译

通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用 GCC 能够很好地管理这些编译单元。假设有一个由 test1.c 和 test2.c 两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序 test,可以使用下面这条命令:
gcc test1.c test2.c -o test
如果同时处理的文件不止一个,GCC 仍然会按照预处理、编译和链接的过程依次进行。如果深究起来,上面这条命令大致相当于依次执行如下三条命令:
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc test1.o test2.o -o test

6.编译成可执行文件

首先我们要进行编译 test.c 为目标文件,这个时候需要执行
gcc –c –I /usr/dev/mysql/include test.c –o test.o

7.链接

最后我们把所有目标文件链接成可执行文件:
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. 编写程序代码

首先,需要编写程序代码并创建三个不同的函数:x2x,x2y,和main函数。这些函数可以写在三个不同的.c文件中。

示例x2x.c:

#include <stdio.h>

void x2x(int x) {
    printf("x2x: %d\n", x * 2);
}

在这里插入图片描述

示例x2y.c(这里假设x2y函数将y的平方打印出来):

#include <stdio.h>

void x2y(int y) {
    printf("x2y: %d\n", y * y);
}

在这里插入图片描述

示例main.c:

void x2x(int x);
void x2y(int y);

int main() {
    int x = 5;
    int y = 3;
    
    x2x(x);
    x2y(y);
    
    return 0;
}

在这里插入图片描述

2. 编译为目标文件

使用gcc编译这三个文件,每个文件生成一个目标文件(.o文件):

gcc -c x2x.c -o x2x.o
gcc -c x2y.c -o x2y.o
gcc -c main.c -o main.o

在这里插入图片描述

3. 创建静态库文件

使用ar工具将x2x.o和x2y.o合并为一个静态库文件(.a文件):

ar rcs libmyfunctions.a x2x.o x2y.o

4. 链接生成可执行程序

使用gcc链接main.o和静态库文件libmyfunctions.a以生成最终的可执行程序:

gcc main.o -L. -lmyfunctions -o myprogram_static

5. 记录文件大小

您可以使用以下命令来查看生成的可执行程序文件的大小:

ls -l myprogram_static

在这里插入图片描述

现在,您已经生成了一个使用静态库的可执行程序。接下来,让我们创建一个动态库并进行对比。

6. 创建动态库文件

使用gcc编译x2x.o和x2y.o生成一个共享对象(动态库)文件(.so文件):

gcc -shared -o libmyfunctions.so x2x.o x2y.o

7. 链接生成可执行程序(使用动态库)

使用gcc链接main.o和动态库文件libmyfunctions.so以生成另一个可执行程序:

gcc main.o -L. -lmyfunctions -o myprogram_dynamic

8. 记录文件大小

同样,您可以使用以下命令来查看生成的动态可执行程序文件的大小:

ls -l myprogram_dynamic

在这里插入图片描述

现在,已经创建了一个使用动态库的可执行程序。可以比较两个程序文件的大小以了解静态库和动态库的差异。通常情况下,动态库的文件大小会比静态库小,因为它只包含函数的链接信息,而不包含实际的函数代码。

六、重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念

编写一个C程序,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程

1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;

2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。下图是一个Cortex-M4的存储器地址映射示意图(与Cortex-M3/stm32F10x基本相同,只存在微小差异)

了解和分析堆、栈、全局、局部等变量的分配地址以及ARM Cortex-M / STM32F10x的存储器地址映射,需要编写两个不同平台下的C程序并进行比较。下面是一个示例C程序,可以在Ubuntu(x86)和STM32(Keil)中运行。

首先,让我们创建一个名为main.c的C程序,该程序在Ubuntu(x86)上运行,演示堆、栈、全局和局部变量的地址分配:

#include <stdio.h>
#include <stdlib.h>

// 全局变量
int globalVar = 10;

int main() {
    // 局部变量
    int localVar = 20;
    static int staticVar = 30;
    int *heapVar = (int *)malloc(sizeof(int));

    printf("Address of globalVar: %p\n", &globalVar);
    printf("Address of localVar: %p\n", &localVar);
    printf("Address of staticVar: %p\n", &staticVar);
    printf("Address of heapVar: %p\n", heapVar);

    free(heapVar);

    return 0;
}

在这里插入图片描述

接下来,创建一个名为stm32_main.c的C程序,该程序在STM32(Keil)上运行:

#include "stm32f10x.h"
#include <stdio.h>

// 全局变量
int globalVar = 10;

int main(void) {
    // 局部变量
    int localVar = 20;
    static int staticVar = 30;
    int *heapVar = (int *)malloc(sizeof(int));

    // 初始化串口
    // 请根据你的STM32型号和串口配置进行修改
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_Mode = USART_Mode_Tx;
    USART_Init(USART1, &USART_InitStructure);
    USART_Cmd(USART1, ENABLE);

    while (1) {
        printf("Address of globalVar: %p\n", &globalVar);
        printf("Address of localVar: %p\n", &localVar);
        printf("Address of staticVar: %p\n", &staticVar);
        printf("Address of heapVar: %p\n", heapVar);

        free(heapVar);

        // 延时或其他操作
    }
}

在这里插入图片描述

运行这两个程序后,将能够比较Ubuntu(x86)和STM32上各种变量的地址分配情况,加深对ARM Cortex-M / STM32F10x存储器地址映射的理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值