GCC运行原理及在Ubuntu和STM32中的程序内存分配问题

gcc相关运行原理及buntu、stm32下的程序内存分配问题

一、任务要求

一. 学习并掌握可执行程序的编译、组装过程。学习任务如下:

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

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

3)将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。

二. Gcc不是一个人在战斗。请说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍。
三.编写一个C程序,重温全局变量、局部变量、堆、栈等概念,在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。
归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析。

二、实验过程

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

2.1.1gcc生成可执行的动态静态库

1、创建hello.h、hello.c 和 main.c文件,输入以下代码

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

gcc -c hello.c

3、创建静态库

创建静态库用 ar 命令。在系统提示符下键入以下命令将创建静态库文件 libmyhello.a

ar -crv libmyhello.a hello.o

4、程序中使用静态数据库

gcc -o main main.c libmyhello.a

5、建立s文件

gcc -c -fpic hello.c gcc -shared *.o -o libsofile.s

6、程序使用动态库

mv libsofile.so /user/lib idconfig

编译运行,程序出现报错。

解决方案:将文件 libmyhello.so 复制到目录

2.1.2生成静态和动态文件并进行链接

1、创建sub1.h、sub2.h、main.c、sub1.c、sub2.c文件:

vi main.c vi sub1.h vi sub2.h vi sub1.c vi sub2.c

对应文件内容如下:

sub1.h:
#include<stdio.h>

int add(int a,int b);

sub2.h:
#include<stdio.h>

int minux(int a,int b);

sub1.c:
#include"sub1.h"

int add(int a,int b){
        return a+b;
}

sub2.c:
#include"sub1.h"

int minus(int a,int b){
        return a-b;
}

main.c:
#include<stdio.h>
#include"sub1.h"
#include"sub2.h"

int main()
{
   int a =1,b =2;
   int c = add(a,b);
   int d = minus(a,b);
   printf("%d\n",c);
   printf("%d",d);
   return 0;
}

2、生成静态库a

gcc -c sub1.c sub2.c

ar crv libafile.a sub1.o sub2.o

使用.a文件创建可执行程序并执行

gcc -o main main.c libafile.a ./main

结果:3、-1

动态库.so文件使用

生成目标文件

gcc -c -fpic sub1.c sub2.c

生成共享.so文件

gcc -shared *.o -o libsofile.so

使用.so库文件,创建可执行程序

gcc -o test main.c libsofile.so ./test

程序出现报错!

解决办法:只在/lib and /usr/lib 下搜索对应 的.so 文件,故需将对应 so 文件拷贝到对应的路径就行了

sudo cp libsofile.so /user/lib

2.2Gcc不是一个人在战斗。请说明gcc编译工具集中各软件的用途,了解EFF文件格式

1、在Linux系统下安装gcc

yum install gcc

2、创建main.c

vi main.c

main.c文件内容如下

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

3、进行预处理过程

将main.c转化为main.i文件

gcc -E main.c -o main.i

4、编译过程

将我们处理的mian.c文件得到的main.i文件做进一步的处理,得到main.s的文件

gcc -S main.i -o main.s

5、汇编

将main.s文件转化为main.o文件

gcc -c main.s -o main.o -v

6、动态链接和静态链接

(1) 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行 文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链 接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完 成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和 重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。

(2) 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统 中把相应动态库加载到内存中去

执行动态链接

gcc main.c -o main

执行静态链接

gcc -static main.c -o main

使用ldd指令看是否链接 了动态库

ldd main

2.3Ubuntu、stm32下的程序内存分配问题

2.31全局变量 & 局部变量

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

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

2.32堆 & 栈

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、程序代码区—存放函数体的二进制代码。
程序如下:

//main.cpp int a = 0; //全局初始化区 int a = 0; //全局初始化区 char *p1; //全局未初始化区 main() { int b; //栈 char s[] = "abc"; //栈 char *p2; //栈 char *p3 = "123456"; //123456\0在常量区,p3在栈上。 static int c = 0; //全局(静态)初始化区 p1 = (char *)malloc(10); p2 = (char *)malloc(20); //分配得来得10和20字节的区域就在堆区。 strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 }

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

编码编写:

#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运行

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

编译结果:Ubuntu在栈区和堆区的地址值都是从上到下增长的

3、Keil运行
keil 环境下默认的内存配置说明

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

修改主函数:

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"

#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(void)
 {		
 	u16 t;  
	u16 len;	
	u16 times=0;
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 //串口初始化为115200
 	LED_Init();			     //LED端口初始化
	KEY_Init();          //初始化与按键连接的硬件接口
 	while(1)
	{
		//定义局部变量
	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;
	}	 
 }

编译报错

原因:声明不能出现在可执行状态之后,C语言关于变量的定义只能放在函数的开头,放在执行语句的前面定义,这是C89的标准。
后来的C99标准就已经改变了,无论定义在之前还是之后都是可以的。

解决办法:点击魔术棒,再点c/c++,打钩上C99 mode ,再使用微库

再次编译,成功!

将代码烧录进入芯片

按下reset键,单片机发送数据

实验结果:stm32的栈区的地址值是从上到下减小的,堆区则是从上到下增长的。

4、结果分析(仍在思考ing)

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

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

参考:【1】[gcc相关运行原理及linux系统下opencv使用_opencv要装gcc吗_辣子鸡味的橘子的博客-CSDN博客](https://blog.csdn.net/qq_52548731/article/details/126950628?ops_request_misc=&request_id=&biz_id=102&utm_term=一. 学习并掌握可执行程序的编译、组装过程。学习任务如下:&utm_medium=distribute.pc_search_result.none-task-blog-2blogsobaiduweb~default-0-126950628.nonecase&spm=1018.2226.3001.4450)

分配,栈是由高地址到低地址,堆是由低地址到高地址

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

参考:【1】[gcc相关运行原理及linux系统下opencv使用_opencv要装gcc吗_辣子鸡味的橘子的博客-CSDN博客](https://blog.csdn.net/qq_52548731/article/details/126950628?ops_request_misc=&request_id=&biz_id=102&utm_term=一. 学习并掌握可执行程序的编译、组装过程。学习任务如下:&utm_medium=distribute.pc_search_result.none-task-blog-2blogsobaiduweb~default-0-126950628.nonecase&spm=1018.2226.3001.4450)

​ 【2】【嵌入式18】Ubuntu、stm32下的程序内存分配问题(堆栈、局部全局变量等)_ubuntu软件使用内存位置_噗噗的罐子的博客-CSDN博客

物联沃分享整理
物联沃-IOTWORD物联网 » GCC运行原理及在Ubuntu和STM32中的程序内存分配问题

发表评论