nanikore
一、学习并掌握可执行程序的编译、组装过程
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
(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