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

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

一. 学习并掌握可执行程序的编译、组装过程

(1)用GCC 生成 .a静态库和 .so动态库

阅读、理解和学习材料“用gcc生成静态库和动态库.pdf”和“静态库.a与.so库文件的生成与使用.pdf”。

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

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

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

在这里插入图片描述

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

静态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为.a。例如:我们将 创建的静态库名为 myhello,则静态库文件名就是 libmyhello.a。在创建和使用静态库时, 需要注意这点。创建静态库用 ar 命令。在系统提示符下键入以下命令将创建静态库文件 libmyhello.a。

ar -crv libmyhello.a hello.o

在这里插入图片描述

4.第 4 步:在程序中使用静态库。

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

方法一:

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 

动态库连接时也可以这样做。

最后结果:

在这里插入图片描述

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

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

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

6.第 6 步:在程序中使用动态库

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

gcc -o hello main.c -L. -lmyhello

直接使用./hello 会报错误,因为找不到动态库文件 libmyhello.so。程序在运行时, 会在/usr/lib 和/lib 等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提 示类似上述错误而终止程序运行。我们将文件 libmyhello.so 复制到目录/usr/lib 中

mv libmyhello.so /usr/lib

在这里插入图片描述

(2)生成静态库并记录文件的大小。

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

1.用vim编辑器分别编写三个.c函数,分别为main1.c、sub1.c、sub2.c

2.在使用gcc分别生成三个.o 文件。

gcc -c main1.c、gcc -c sub1.c、gcc -c sub2.c

3.将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序

ar -crv libmysub1.a sub1.o

ar -crv libmysub2.a sub2.o

gcc main1.c libmysub1.a libmysub2.a -o sub1

./sub1

在这里插入图片描述

4.查看文件大小,为8424kb。

在这里插入图片描述

(3)生成动态库并记录文件的大小。

1.首先通过.o文件生成两个.so文件

gcc -shared -fPIC -o libmysub1.so sub1.o

gcc -shared -fPIC -o libmysub2.so sub2.o

2.再生成目标文件

gcc main1.c libmysub1.so libmysub2.so -o sub1

3.运行目标文件并记录大小,.so 文件的大小为8360kb。

在这里插入图片描述

二. Gcc不是一个人在战斗。

请说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍。

(1)实质上编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译 (Compilation)、汇编 (Assembly)和连接(Linking)

1.预处理:可以输出 test.i 文件中存放着 test.c 经预处理之后的代码。gcc 的-E 选项,可以让编译器在预处理后停止,并输出预处理结果。

gcc -E test.c -o test.i 或 gcc -E test.c

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

gcc -S test.i -o test.s

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

在这里插入图片描述

(2)多个程序文件的编译,如果同时处理的文件不止一个,GCC 仍然会按照预处理、编译和链接的过程依次进行。

gcc -c test1.c -o test1.o 

gcc -c test2.c -o test2.o 

gcc test1.o test2.o -o test

(3)分析 ELF 文件:ELF 文件的段,位于 ELF Header 和 Section Header Table 之间的都 是段(Section)。

.text:已编译程序的指令代码段。

.rodata:ro 代表 read only,即只读数据(譬如常数 const)。

.data:已初始化的 C 程序全局变量和静态局部变量。

.bss:未初始化的 C 程序全局变量和静态局部变量。

.debug:调试符号表,调试器用此段的信息帮助调试。

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

三. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念。

(1)题目:

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

(2)全局变量 & 局部变量

全局变量:在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件。

局部变量:定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数的内部就是无效的,再使用就会报错。

区别:

全局变量局部变量
定义位置在方法外部,直接写在类中在方法内部
作用范围整个类中都可以使用只能在方法中使用
默认值如果没有赋值,则有默认值,规则同数组没有默认值,要使用必须手动赋值
内存位置位于堆内存位于栈内存

(3)堆 & 栈

1、STM32中的堆和栈

单片机是一种集成电路芯片,集成CPU、RAM、ROM、多种I/O口和中断系统、定时器/计数器等功能。CPU中包括了各种总线电路,计算电路,逻辑电路,还有各种寄存器。

stm32 有通用寄存器 R0‐ R15 以及一些特殊功能寄存器,其中包括了堆栈指针寄存器。
当stm32正常运行程序的时候,来了一个中断,CPU就需要将寄存器中的值压栈到RAM里,然后将数据所在的地址存放在堆栈寄存器中。
等中断处理完成退出时,再将数据出栈到之前的寄存器中,这个在C语言里是自动完成的。

2、程序的内存分配

一般程序占用的内存分为以下几个部分:

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。它与数据结构中的堆是两回事,分配方式类似于链表。

3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后有系统释放

4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放

5、程序代码区—存放函数体的二进制代码。

3、各区存放位置

RAM
RAM又称随机存取存储器,存储的内容可通过指令随机读写访问。RAM中的存储的数据在掉电是是会丢失,因而只能在开机运行时存储数据。其中RAM又可以分为两种,一种是Dynamic RAM(DRAM动态随机存储器),另一种是Static RAM(SRAM,静态随机存储器)。

ROM
ROM又称只读存储器,只能从里面读出数据而不能任意写入数据。ROM与RAM相比,具有价格高,容量小的缺点。但由于其具有掉电后数据可保持不变的优点,因此常用也存放一次性写入的程序和数据,比如主版的BIOS程序的芯片就是ROM存储器。

Flash Memory
由于ROM具有不易更改的特性,后面就发展了Flash Memory。Flash Memory不仅具有ROM掉电不丢失数据的特点,又可以在需要的时候对数据进行更改,不过价格比ROM要高。

4、关于堆(stack)和栈(heap)详细比较

申请方式
堆:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间

栈:
需要程序员自己申请,并指明大小,在c中malloc函数,如p1 = (char *)malloc(10),在C++中用new运算符。

申请和系统的响应
栈:
只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:
首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

申请大小的限制
栈:
在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

申请效率的比较
栈:
由系统自动分配,速度较快。但程序员是无法控制的。

堆:
是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

堆和栈中的存储内容
栈:
在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:
一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

通俗解释
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就像是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

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

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;
}
2.Ubuntu运行

将上面的代码放入vim文本编辑器中,进行编译

在这里插入图片描述

可以发现,Ubuntu在栈区和堆区的地址值都是从上到下增长的。

3.Keil运行

在这里插入图片描述

在这里插入图片描述

可以发现,stm32的栈区的地址值是从上到下减小的,堆区则是从上到下增长的

4.结果分析

一般而言,程序内变量在堆栈上的分配,栈是由高地址到低地址,堆是由低地址到高地址

在Ubuntu下,栈区的地址存储是向上增长,堆区的地址存储也是向上增长;
在STM32下,栈区的地址存储是向下增长,堆区的地址存储却是向上增长。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值