GCC工具使用 重温C程序内存分配 Ubuntu与keil下stm32对比

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

1、程序仿做1

参考“用gcc生成静态库和动态库”和“静态库.a与.so库文件的生成与使用”

我们通常把一些公用函数制作成函数库,供其他程序使用,函数库分为静态库和动态库两种。静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。相反,动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要动态库存在。

(1)编辑生成例子程序hello.h\hello.c和main.c。

vim hello.h

在这里插入图片描述

vim hello.c

在这里插入图片描述

vim main.c

在这里插入图片描述

(2)将hello.c编译成.o文件

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

gcc -c hello.c

在这里插入图片描述
如图,成功生成hello.o文件

(3)由.o文件创建静态库。

命名规范:以lib为前缀,紧接着跟静态库名,拓展名为.a。例如:我们将创建的静态库名为myhellp,例如libmuhello.a。

//创建静态库用ar命令。
ar -crv libmyhello.a hello.o

在这里插入图片描述

(4)在程序中使用静态库

到现在,静态库已经制作完成,接下来使用其内部的函数,只需要在使用到了这些公用函数的源程序中包含这些公用函数的原型声明,然后再用gcc命令生成目标文件时指明静态库名,gcc将会从静态库中将公用函数连接到目标文件中。

生成目标函数
方法一

gcc -o hello main.c -L. -lmyhello		//这里名字可以简写

在这里插入图片描述
方法二

gcc main.c libmyhello.a -o hello

在这里插入图片描述
方法三

//先生成main.o
gcc -c main.c
//再生成可执行文件
gcc -o hello main.o libmyhello.a

在这里插入图片描述

(5)由.o文件创建动态库文件

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

生成动态库文件libmyhello.so

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

在这里插入图片描述

(6)在程序中使用动态库:

方法和静态库的使用相同,在用到了这些公共函数的源程序中包含这些公用函数的原型声明,然后在用gcc命令生成目标文件时指明动态库名进行编译。

//生成目标文件
gcc -o hello main.c -L. -lmyhello

生成目标文件后直接运行hello程序会出错,因为程序在运行时会在/usr/lin和/lib等目录中查找需要的动态库文件,找到才会载入动态库
在这里插入图片描述
将libmyhello.so复制到/usr/lib中
这里遇到了点小问题,提示Permission denied,需要给普通用户添加sudo权限

sudo -i 	//切换超级管理员
visudo		//弹出文本框找到下面图片中的位置添加一行idlike切换为自己用户名

在这里插入图片描述
之后就可以使用了,(要在命令前加sudo 例如sudo libmyhello.so /usr/lib)
在这里插入图片描述

libmuhello.so复制到 /usr/lib目录下之后成功调用程序

在这里插入图片描述

检验当静态库和动态库同名时,gcc会使用那个库文件
先删掉.c和.h外的所有文件。
在这里插入图片描述

//然后创建静态库文件libmyhello.a和动态库文件libmyhello.so
gcc -c hello.c
ar -cr libmyhello.a hello.o
gcc -shared -fPIC -o libmyhello.so hello.o

在这里插入图片描述
提示找不到目标文件因为我们已经把/usr/lib中的删除了,证明动态库和静态库同时存在先使用的是动态库
当然可以gcc -o hello main.c -L. -lmyhello指定使用动态库

在这里插入图片描述

2、改写第一次程序

(1)添加x2y函数

#include<stdio.h>

int x2y(int a,int b)
{
        int c;
        c=a>>b;
        return c;
}

(2)改写main.c

#include<stdio.h>

int x2x(int a,int b);
int x2y(int a,int b);

int main()
{
        int a,b;
        int c;
        scanf("%d %d",&a,&b);
        c=x2x(a,b);
        c=x2x(c,b);
        printf("%d",c);
        return 0;
}

最终有三个函数

在这里插入图片描述

(3)将三个函数sub1.c sub2.c main.c用gcc编译为三个.o文件

//编译sub1.c
gcc -c sub1.c
//编译sub2.c
gcc -c sub2.c
//编译main.c
gcc -c main.c

![在这里插入图片描述](https://img-blog.csdnimg.cn/37a0104b2db640c9ad30ce54080a2f33.png

(4)生成静态库文件并链接

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

在这里插入图片描述

(5)记录文件的大小

在这里插入图片描述

3、动态库使用

(1)生成动态库文件并链接

将x2x\x2y目标文件用ar工具生成一个.so动态库文件并用gcc将main函数的目标文件与此动态库链接
在这里插入图片描述

(2)记录大小

在这里插入图片描述

二、程序仿做2

目标了解gcc编译工具集中各软件的用途,了解EFF文件格式。参照“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”

GCC原本是GNU C Complier的缩写,只能支持C,发展到现在可以支持例如C++,Java,Objective、Pascal等语言。

1、GCC编译工具集中各软件的用途

(1)简单编译

编译一段简单的代码

#include<stdio.h>
int main(void)
{
	printf("Hello World!\n");
	return 0;
}

可以一步到位编译

gcc test.c -o test

也可以分四步编译

//1预处理生成.i后缀文件
gcc -E test.c -o test.i //或gcc -E test.c
//2编译为汇编代码生成.s文件
gcc -S test.i -o test.s
//3汇编生成.o文件
gcc -c test .s -o test.o
//链接生成可执行程序
gcc test.o -o test

在我写的这一篇文章中有详细介绍,在这里简单介绍
GCC C程序 生成可执行程序

(2) 多个程序文件的编译

假设一个由test1.c和test2.c两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序test,使用以下命令。

//如果同时处理的文件不止一个,GCC仍然会按照预处理,编译和链接的过程依次执行。
gcc test1.c test2.c test

//上面的命令可以分解为以下几步
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc test1.o test2.o -o test

(3)检错

-pedantic选项能帮助程序员发现一些不符合ANSI/ISO C便准的代码,但不是全部,只有设定为需要诊断的呢,才有可能被GCC发现并提出警告。

gcc -pedantic illcode.c -o illcode

-Wall也能使GCC产生尽可能多的警告信息。

gcc -Wall illcode.c -o illcode

-Werror选项可以让GCC在所有产生警告的地方停止编译,强迫程序员修改每一个有警告的地方

gcc -Werror test.c -o test

2、EFF文件格式

一个ELF文件包含下面几个段
.test:已编译程序的指令代码段。
.rodate:ro代表read only,即只读数据(譬如常数const)。
.data:已初始化的C程序全局变量和静态局部变量。
.debug:调试符号表,调试器用此段的信息帮助调试。

(1)用readelf -S 查看各个section

//这里查看之前用静态库生成的可执行文件main
readelf -S main

在这里插入图片描述

(2)反汇编ELF

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

objdump -D main

在这里插入图片描述
objdump -S反汇编并混合C语言源代码显示

objdumo -S main

在这里插入图片描述

三、编写C程序,重温全局常量、局部变量、静态变量、堆、栈等概念、在Ubuntu(x86)和STM32(keil)中分别进行编程、验证。

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

(1)C程序的内存分配

  • 栈区(stack)
    由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap)
    一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。它与数据结构中的堆不同,分配方式类似于链表。
  • 全局区(静态区)(static)
    全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。当程序结束后,变量由系统释放 。
  • 文字常量区
    存放常量字符串。当程序结束后,常量字符串由系统释放 。
  • 程序代码区
    存放函数体的二进制代码。

(2)代码举例理解

例子(一)

int a = 0; //全局区

void main()

{

int b; //栈

char s[] = "abc"; //s在栈,"abc"在文字常量区

char *p1,*p2; //栈

char *p3 = "123456"; //"123456"在常量区,p3在栈上

static int c =0; //全局区

p1 = (char *)malloc(10); //p1在栈,分配的10字节在堆

p2 = (char *)malloc(20); //p2在栈,分配的20字节在堆

strcpy(p1, "123456"); //"123456"放在常量区

//编译器可能将它与p3所指向的"123456"优化成一个地方。

}

例子(二)



//返回char型指针

char *f()

{

//s数组存放于栈上

char s[4] = {'1','2','3','0'};

return s; //返回s数组的地址,但函数运行完s数组就被释放了

}

void main()

{

char *s;

s = f();

printf ("%s", s); //打印出来乱码。因为s所指向地址已经没有数据

}

2对比Ubuntu与keil 下stm32上运行的内存分配

(1)ubuntu下运行代码

#include <stdio.h>
#include <stdlib.h>
int k1 = 1;
int k2;
static int k3 = 2;
static int k4;
int main( )
{   static int m1=2, m2;
    int i = 1;
    char *p;
    char str[10] = "hello";
    char *var1 = "123456";
    char *var2 = "abcdef";
    int *p1=malloc(4);
    int *p2=malloc(4);
    free(p1);
    free(p2);
    printf("栈区-变量地址\n");
    printf("                i:%p\n", &i);
    printf("                p:%p\n", &p);
    printf("              str:%p\n", str);
    printf("\n堆区-动态申请地址\n");
    printf("                   %p\n", p1);
    printf("                   %p\n", p2);
    printf("\n.bss段\n");
    printf("全局外部无初值 k2:%p\n", &k2);
    printf("静态外部无初值 k4:%p\n", &k4);
    printf("静态内部无初值 m2:%p\n", &m2);
    printf("\n.data段\n");
    printf("全局外部有初值 k1:%p\n", &k1);
    printf("静态外部有初值 k3:%p\n", &k3);
    printf("静态内部有初值 m1:%p\n", &m1);
    printf("\n常量区\n");
    printf("文字常量地址     :%p\n",var1);
    printf("文字常量地址     :%p\n",var2);
    printf("\n代码区\n");
    printf("程序区地址       :%p\n",&main);
    return 0;
}


这里可以发现,Ubuntu中栈区变量地址从上到下递增,堆区动态申请地址也由上到下递增
在这里插入图片描述

(2)keil下stm32上运行

stm32下运行代码

Serial.c 串口初始化函数Serial_Init

void Serial_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Tx;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_Init(USART1, &USART_InitStructure);
	
	USART_Cmd(USART1, ENABLE);
}

main函数

#include "stm32f10x.h"                  // Device header
//#include "Delay.h"
//#include "OLED.h"
#include "Serial.h"
#include <stdio.h>
#include <stdlib.h>//加这一句
int k1 = 1;
int k2;
static int k3 = 2;
static int k4;
int main( )
{   
	//OLED_Init();
	Serial_Init();
	static int m1=2, m2;
    int i = 1;
    char *p;
    char str[10] = "hello";
    char *var1 = "123456";
    char *var2 = "abcdef";
    int *p1=malloc(4);
    int *p2=malloc(4);
    free(p1);
    free(p2);
    printf("栈区-变量地址\n");
    printf("                i:%p\n", &i);
    printf("                p:%p\n", &p);
    printf("              str:%p\n", str);
    printf("\n堆区-动态申请地址\n");
    printf("                   %p\n", p1);
    printf("                   %p\n", p2);
    printf("\n.bss段\n");
    printf("全局外部无初值 k2:%p\n", &k2);
    printf("静态外部无初值 k4:%p\n", &k4);
    printf("静态内部无初值 m2:%p\n", &m2);
    printf("\n.data段\n");
    printf("全局外部有初值 k1:%p\n", &k1);
    printf("静态外部有初值 k3:%p\n", &k3);
    printf("静态内部有初值 m1:%p\n", &m1);
    printf("\n常量区\n");
    printf("文字常量地址     :%p\n",var1);
    printf("文字常量地址     :%p\n",var2);
    printf("\n代码区\n");
    printf("程序区地址       :%p\n",&main);
    return 0;
}

用SSCOM串口助手接收到的是乱码,勾选接受数据到文件,然后转格式用utf-8格式显示在这里插入图片描述

在这里插入图片描述
这里可以发现stm32在栈区变量地址由上到下递减,堆区由上到下递增。

(3)分析总结

归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析
加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解

linux中虚拟地址空间布局
名称存储内容
动态分配的内存
局部变量、函数参数、返回地址等
BSS段未初始化或初始值为0的全局变量和静态局部变量
数据段已初始化且初值非0的全局变量和静态局部变量
代码段可执行代码、字符串字面值、只读变量

在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。
BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。

堆栈地址分配

stm32中
栈:向低地址扩展
堆:向高地址扩展
Ubuntu中
均向低地址扩展

stm32中写程序需要注意

局部变量不要太大太多,如局部数组,超过某个数量需定义为全局数组,因为局部数组同样储存在堆栈中。

参考链接
https://www.cnblogs.com/aimenfeifei/p/4238705.html
https://blog.csdn.net/qq_43279579/article/details/110308101
https://www.bilibili.com/video/BV1th411z7sn
https://blog.csdn.net/zhangskd/article/details/6956638
https://blog.csdn.net/FreeeLinux/article/details/53782986
https://blog.csdn.net/u011784994/article/details/53157614

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值