bss_data_text段介绍
1.简介
- 在C语言等编程语言中,程序的内存布局通常包括代码段 (.text) 、数据段 (data) 、BSS段(Block Started by Symbol) 和堆栈(Stack) 等部分。
-
C语言程序与编译后的目标文件有如下的对应关系:
如下是STM32的程序组件分类:
2 bss段
bss段(bss segment,bss是英文Block Started by Symbol的简称),bss段属于静态内存分配。
存放程序中未初始化的全局变量和静态局部变量。BSS段中的变量在程序开始执行前没有被赋予任何值,通常这些变量会被操作系统或链接器初始化为0。BSS段不占用可执行文件的空间,它的存在只是记录了这些变量所占用的内存大小;
当程序运行时,操作系统会负责将BSS段初始化为0。这样的内存布局有助于优化程序的加载时间和减少程序的内存占用
3 data段
数据段(data segment),数据段属于静态内存分配。
指存放程序中已初始化的全局变量和静态局部变量、非const的全局变量的一块内存区域;程序中已经初始化的非零的全局变量和已经初始化的非零的静态局部变量(static)
注:const全局变量一般放到了rodata段,初始化为零的全局变量可能被编译器优化到 bss段
4 text段
代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域,程序执行时CPU会读取并执行的机器指令。
代码段大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。
在代码段中,可能包含一些只读的常数变量,例如字符串常量等。
注:1.text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。
2.text段一般也包含rodata段,text段可以存放到flash中;data段也放到flash中,运行时由flash中加载到SRAM;bss段在运行时系统创建,存放到RAM中
5 总结
未初始化的全局变量、静态局部变量,存储在.bss段中,具体体现为一个占位符;
已初始化的全局变量、静态局部变量,存储在.data段中;
此外,非静态局部变量,都在栈中分配空间。
.bss 是不占用.exe文件空间的,其内容由操作系统初始化(清零);
.data 却需要占用,其内容由程序初始化。
bss 段,不为数据分配空间,只是记录数据所需空间的大小;
bss 段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在data段后面。
data 段,则为数据分配空间,数据保存在目标文件中; data 段包含经过初始化的全局变量以及它们的值。
5 程序运行时的概念介绍
5.1 堆
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);
当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
5.2 栈
栈又称堆栈,用于存放程序中的非静态局部变量和函数调用时的临时变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。
除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场,当函数调用时返回地址、函数参数以及局部变量等都会存储在栈上。
从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
5.3 编译生成的可执行文件在内存中的布局
5.4 案例介绍
1.容量演示
int bss_array[1024 * 1024] = {0};
int main(int argc, char* argv[])
{
return 0;
}
gcc -g bss.c -o bss.exe
//变量bss_array的大小为4M,而生成的可执行文件的大小只有5K。由此可见,bss类型的全局变量只占运行时的内存空间,而不占文件空间
int bss_array[1024 * 1024] = {1};
int main(int argc, char* argv[])
{
return 0;
}
gcc -g data.c -o data.exe
//仅仅是把初始化的值改为非零了,生成的文件就会变为4M多。由此可见,data类型的全局变量是即占文件空间,又占用运行时内存空间的
2.
如下定义一个数组变量,容量大于72K,大于了stm32单片机当前型号的ram容量;编译的时候就报错了; 初始化非0的变量存在.data段;
3.
5.5 stm32 单片机编译后的内存布局介绍
map.txt说明
startup.S启动文件和汇编介绍
当今强大的编译器将C或者更高级的语言编译成机器码后,其效能损失已经很小了,再加上芯片的性能越来越强,让汇编语言显得可有可无。但对于嵌入式来说至少在下面两种情况还需要汇编:1是启动代码,2是0S的上下文切换。另外在极端情况下使用汇编提高效率也是有必要的,例如芯片内核非常新编译器优化不够好可以在非常清楚CPU的微结构下进行指令集编码提高性能。
JL的简单启动文件
.section .stack, "a" //_stack、_ustack被放入到.stack段,"a" :段可以被装载到内存的任何位置并运行
.space 0x600 //分配0x600字节的数据空间
.global _stack
_stack: //栈结尾,.stack为起始地址
.space 0x600
.global _ustack
_ustack: //栈结尾
.section .code_entry
.align 2 //表示将当前PC地址推进到2^2=4个字节对齐的位置处
.global _start //_start被放入到.code_entry段
.extern main //声明main函数是外部的
.type _start,@function //_start符号定义为一个函数(function)
.org 0 //下一条指令的装入地址为0。
_start:
sp = _stack
usp = _ustack
r3 = bss_begin ;//movh r0, bss_begin = ADDR(.bss );.bss的起始地址
r1 = 0 ;//mov r1,0
r2 = bss_size ;//movh r2, bss_size = SIZEOF(.bss);.bss的大小
r2 >>= 2; //bss_size/4,因为寄存器是32位的
1:
rep r2{ //REP(Repeat)汇编指令是一种重复执行指令的控制指令
[r3++] = r1 //把整个.bss段地址置0
}
if(r2 != 0) goto 1b //不为0,继续执行rep
reti = main //RETI是中断服务子程序的返回指令,还能清除内部相应的中断状态寄存器
rti //RTI中断返回指令需要完成堆栈操作,RTI中断返回指令需要完成堆栈操作
常见汇编用法
1、
.type name , type description
.type伪操作用于定义符号的类型。譬如“.type symbol,@function”即将名为symbol的符号定义为一个函数(function)。
2、
.align integer.align伪操作用于将当前PC地址推进到“2的integer次方个字节”对齐的位置。譬如“.align 3”即表示将当前PC地址推进到8个字节对齐的位置处。
3、
.section name [, subsection].section伪操作指明将接下来的代码汇编链接到名为name的段(Section)当中,还可以指定可选的子段(Subsection)。常见的段如.text、.data、.rodata、.bss:
“.section .text”伪操作将接下来的代码汇编链接到.text段。
“.section .data”伪操作将接下来的代码汇编链接到.data段。
“.section .rodata”伪操作将接下来的代码汇编链接到.rodata段。
“.section .bss”伪操作将接下来的代码汇编链接到.bss段。.text伪操作基本等效于“.section .text”。
.data伪操作基本等效于“.section .data”。
.rodata伪操作基本等效于“.section .rodata”。
.bss伪操作基本等效于“.section .bss”。4、
.weak symbol_name在汇编程序中,符号的默认属性为强(strong),.weak伪操作则用于设置符号的属性为弱(weak),如果此符号之前没有定义过,则同时创建此符号并定义其属性为weak。
如果符号的属性为weak,那么它无需定义具体的内容。在链接的过程中,另外一个属性为strong的同名符号可以将此weak符号的内容强制覆盖。
利用此特性,.weak伪操作常用于预先预留一个空符号,使得其能够通过汇编器语法检查,但是在后续的程序中定义符号的真正实体,
并且在链接阶段将空符号覆盖并链接.5、
.global symbol_name或者.globl symbol_name.global和.globl伪操作用于定义一个全局的符号,使得链接器能够全局识别它,即一个程序文件中定义的符号能够被所有其他程序文件可见。
6、
.local symbol_name
.local伪操作用于定义局部符号,使得此符号不能够被其他程序文件可见7、.org
.org 0x1000
mov r0, #10
上述代码中,“.org 0x1000” 设置了下一条指令(“mov r0, #10”)的装入地址为0x1000。
也就是说,当这个汇编文件被链接并加载到内存中时,"mov r0, #10"这条指令的地址将会是0x1000。
org 指令是链接时使用的,不是汇编那一步使用的。即不是cpu的一条指令,而是给编译器看的伪指令8、.section .stack, "a"
a section is allocatable,可以将 allocatable 理解为 relocatable,也就是段可以被装载到内存的任何位置并运行
.ld文件介绍
1 .ld文件的作用
程序代码(.s 和 .c)源文件会经过预编译、编译、汇编、链接最后生成目标可执行文件,.ld文件是作用在链接过程。
链接的作用是:
合并各个.obj文件的section,合并符号表,进行符号解析;
符号地址重定位;
生成可执行文件。
linker_flash.ld:Flash和SRAM内存分配,为Flash构建目标分配代码段和数据段;
linker_ram.ld:SRAM内存分配,为RAM构建目标分配代码段和数据段。
可以利用.ld文件将函数和变量放置到自定义的地址中。
.ld文件的解析:按照常用关键词去解析.ld文件内容即可
2 常用的关键词介绍
ENTRY命令:
运行一个程序时第一个被执行到的指令的"入口点"。
MEMORY命令:
内存块配置命令,一个连接脚本最多一个’MEMORY’命令。
SECTIONS命令:
’段’命令,段中又包含多个’节’, SECTIONS命令告诉连接器如何把输入节映射到输出节, 如何把输入节放入到内存中。
KEEP()命令:
防止垃圾收集机制把这个节排除在外,同时保证向量表在段中的位置处于最顶端。
ALTGN命令:
以多少位对齐,例如ALTGN(4)表示以4位对齐。
.命令:
一个点“.”可以用来获取当前内存地址。
*:
‘’是一个通配符,可以与所有文件名匹配。例如表达式(.text)表示所有输入文件的.text输入段。
3 指定地址操作
(1)指定变量地址支持的语法语法
__attribute__ ((section (".SectionName"))) uint32_t value=0x01;//因此需要自定义一个节用来存放变量或函数。
注:无法直接将变量或函数直接存到绝对地址(指定地址语法见下),如:
__attribute__ ((at (0x10000000))) uint32_t value = 0x01;
会出现此警告:
表示‘at’后面的指定地址方式不支持,因此忽略掉了。
PROVIDE关键字语法
想象某些情况下, 一个符号被引用到的时候只在连接脚本中定义,而不在任何一个被连接进来的目标文件中定义.这种做法比较明智.比如, 传统的连接器定义了一个符号’etext’. ANSI C 需要用户能够把’etext’作为一个函数使用,这时不会产生错误.
‘PROVIDE’关键字可以被用来定义一个符号,比如’etext’, 这个定义只在它被引用到的时候有效,而在它被定义的时候无效.
语法:`PROVIDE(SYMBOL = EXPRESSION)'.
举例:
_etext = .;
PROVIDE(etext = .);在这个例子中, 如果程序定义了一个’_etext’(带有一个前导下划线), 连接器会给出一个重定义错误. 如果,程序定义了一个’etext’(不带前导下划线), 连接器会默认使用程序中的定义. 如果程序引用了’etext’但不定义它,连接器会使用连接脚本中的定义
4 实例说明
正确写法
SECTIONS
{
. = ORIGIN(sram);
.boot :
{
*(.chip_entry)
*(.text*)
*(.rodata*)
. = ALIGN(32);
*(.data*)
. = ALIGN(4);// . = (. + 511) / 512 * 512 ;//对齐操作
g_burn_config_data = . ;//占用一个位置,当前的地址复位该变量
} > sramPROVIDE(g_burn_config_data = .);
. += 0x40;//为g_burn_config_data开始的地址分配64字节空间,用于传参.bss ALIGN(32) (NOLOAD):
{....
}
有问题写法:下面这种写法会导致生成的可执行文件.isp数据最后多了64字节的0
SECTIONS
{
. = ORIGIN(sram);
.boot :
{
*(.chip_entry)
*(.text*)
*(.rodata*)
. = ALIGN(32);
*(.data*)
. = ALIGN(4);
g_burn_config_data = . ;PROVIDE(g_burn_config_data = .);
. += 0x40;
} > sram.bss ALIGN(32) (NOLOAD):
{....
}
编译工具链介绍
1. gcc/g++ 编译流程详解:
gcc -lstdc++ main.cpp 直接从源代码到目标可执行文件了,把过程拆分:
预处理 :gcc -E main.cpp > main.ii
编译 : gcc -s main.i 得到名为 main.s 的汇编文件汇编 :gcc-c main.s 得到名为 main.o(.obj)的二进制文件
链接 :gcc -lstdc++ main.0 得到名为 a.out 的可执行文件
可以参考:
Makefile从入门到项目编译实战
2.stm32编译工具链
【1】简介:
编译调用工具链过程本质就是让命令行通过“PATH”路径找到“fromelf.exe”等程序运行!!!
默认运行“fromelf.exe” 时它会输出自己的帮助信息,这就是工具链的调用过程,集成开发环境keil-MDK 本质上也是如此调用工具链的,只 是它集成为 GUI,相对于命令行对用户更友好,毕竟上述配置环境变量的过程已经让新手烦躁 了
【2】查看帮助信息:
【3】列如:
“–cpu list”可列出编译器支持的所有 cpu,我们在命令行中输入“armcc –cpu list”,可查看图 cpulist 中的 cpu 列表:
【4】MDK编译选项配置,自动生产编译命令;编译过程就是调用这些命令!
图中配置的编译选项,调用了-c、-cpu –D –g –O1 等编译选项,当我们修改 MDK 的编译配 置时,可看到该控制命令也会有相应的变化;
【5】armar 工具用于把工程打包成库文件;不想源代码被别人看到!
【6】fromelf工具生成hex,bin文件
下面的命令同勾选“Create HEX file”效果是一样的!
正常情况需要使用相对路径!
不想通过勾选生成,就使用cmd命令生成:
单片机运行原理
1 单片机的组成
以8051单片机进行介绍(相对简单),其内部硬件结构包括:
中央处理器CPU:它是单片机内部的核心部件,决定了单片机的主要功能特性,由运算器和控制器两大部分组成。
存储器:8051单片机在系统结构上采用了哈佛型,将程序和数据分别存放在两个存储器内,一个称为程序存储器,另一个为数据存储器在物理结构上分程序存储器和数据存储器,有四个物理上相互独立的存储空间,即片内ROM和片外ROM,片内RAM和片外RAM。
定时器/计数器(T/C):8051单片机内有两个16位的定时器/计数器,每个T/C既可以设置成计数方式,也可以设置成定时方式,并以其定时计数结果对计算机进行控制。
并行I/O口:8051有四个8位并行I/O接口(P0~P3),以实现数据的并行输入输出。
串行口:8051单片机有一个全双工的串行口,可实现单片机和单片机或其他设备间的串行通信。
中断控制系统:8051共有5个中断源,非为高级和低级两个级别它可以接收外部中断申请、定时器/计数器申请和串行口申请,常用于实时控制、故障自动处理、计算机与外设间传送数据及人机对话等。
2 单片机启动过程
1.单片机的启动过程是加电后,先运行芯片内部固有程序(这个程序是用户访问不到也改写不了的),生产芯片时就固化该程序,即启动代码。启动代码程序建立完运行环境后,会去读串口状态,就是用户下载程序用到的各个端口,判断用户是否正在使用端口准备下载程序。
2.如果是,就按用户要求,把用户程序下载到指定地址上。如果不是,就跳转到已经下载过的用户程序入口,从而把芯片控制权交给用户程序。如果是新的芯片还没有下载过,那么就停留在读取串口状态的循环中。
3.启动代码通常都烧写在flash中,它是系统一上电就执行的一段程序,它运行在任何用户C代码之前。上电后,arm处理器处于arm态,运行于管理模式,同时系统所有中断被禁止,PC到地址0处取指令执行。
4.一个可执行映像文件必须有个入口点,而能放在rom起始处的映像文件的入口地址也必须设置为0。在汇编语言中,可以自行定义定义一个程序的入口点,当工程中有多个入口点时,需要在连接器中使用
-entry
指出程序的入口点。5.如果用户创建的程序中,包含了
main
函数,则与C库初始化代码对应的也会有个入口点。总的来说,启动代码主要完成两方面的工作,一是初始化执行环境,例如中断向量表、堆栈、I/O等;二是初始化c库和用户应用程序。6.在第一阶段,启动代码的过程可以描述为:
建立中断向量表;
初始化存储器;
初始化堆栈寄存器;
初始化i/o以及其他必要的设备;
根据需要改变处理器的状态。
PC电脑这些带系统的设备在上电时,和单片机处理过程差不多,只不过他们是读取的BIOS,有它完成了很多初始化操作,最后,调用系统的初始化函数,将控制权交给了操作系统,于是我们看到了Windows,Linux系统启动了。
如果将操作系统看作是在处理器上跑的一个很大的裸机程序(就是直接在硬件上跑的程序,因为操作系统就是直接跑在CPU上的),那么操作系统的启动很像MCU程序的启动。前者有一个很大的初始化程序完成很复杂的初始化,后者有一段不长的汇编代码完成一些简单的初始化。
如果是系统上的程序启动呢?它们是由系统来决定的,Linux上在shell下输入
./p
后,首先检查是否是一个内建的shell命令;如果不是,则shell假设他是一个可执行文件(Linux上一般是elf格式),然后调用一些相关的函数,将在硬盘上的p文件的内容拷贝到内存(DDR RAM)中,并建立一个它的运行环境(当然这里边还有内存映射,虚拟内存,连接与加载,等一些其他东西),准备执行。由以上可知,单片机上的程序和平时在系统上运行的程序,在启动时差异是很大的,如果将程序调用main以前的动作,都抽象为初始化的话,程序的启动可以简化为:建立运行环境+调用main函数,这样程序的执行差异是不大的。
因为单片机上跑的程序(裸机程序),是和操作系统一样跑在硬件上的,它们属于一个层次的。过去之所以没有区分出单片机上的程序和PC机上的程序的一些差异,就是没有弄明白这一点。
3 程序的执行过程
单片机中一个程序的运行过程分为取指令,分析指令和执行指令几个步骤。
取指令的任务是:根据程序计数器PC中的值从程序存储器读出现行指令,送到指令寄存器。
分析指令阶段的任务是:将指令寄存器中的指令操作码取出后进行译码,分析其指令性质。如指令要求操作数,则寻找操作数地址。
计算机执行程序的过程实际上就是逐条指令地重复上述操作过程,直至遇到停机指令可循环等待指令。
虽然在《微型计算机原理》课上知道程序运行时,从内存中读取指令和数据进行执行和回写。但是单片机上只有几K的RAM,而flash一般有几十K甚至1M,这个时候指令和数据都在内存中吗?
这里指的内存仅指RAM,因为PC上我们常说的内存就是DDR RAM memory,先入为主以至于认为单片机上也是这样,还没有明白其实RAM和Flash都是内存。
这不可能,因为课上老师只说内存,但是PC上内存一般就是DDR RAM,不会是硬盘,硬盘是保存数据的地方;由此类比时,自己把自己弄晕菜了,单片机的RAM对应于DDR RAM,那Flash是不是就对应于硬盘了呢?在CSAPP上明白了,PC上之所以都在DDR RAM上,是速度的因素。
硬盘的速度太慢,即使是即将到来的SSD比起DDRRAM,还是差着几个数量级,所以拷贝到DDRRAM中。这时,一个程序的代码和数据是连续存放的,其中代码段是只读区域,数据段是可读写区域(这是由操作系统的内存管理机制决定的)。
运行时,再将它们拷贝到速度更快的SRAM中,以得到更快的执行速度。而对于,单片机而言工作频率也就几M,几十M,从Flash中与从RAM中读的差异可能并不明显,不会成为程序执行的瓶颈(而对于PC而言,Flash的速度太慢,DDRRAM的速度也是很慢,即使是SRAM也是慢了不少,于是再提高工作频率也提高不了程序的执行速度,所以现在CPU工作频率最快是在2003左右,一个瓶颈出现了。
8051单片机运行举例
开机时,程序计算器PC变为0000H
。然后单片机在时序电路作用下自动进入执行程序过程。执行过程实际上就是取出指令(取出存储器中事先存放的指令阶段)和执行指令(分析和执行指令)的循环过程。
例如执行指令:MOV A,#0E0H
,其机器码为74H E0H
,该指令的功能是把操作数E0H
送入累加器,0000H
单元中已存放74H
,0001H
单元中已存放E0H
。当单片机开始运行时,首先是进入取指阶段,其次序是:
程序计数器的内容(这时是
0000H
)送到地址寄存器;程序计数器的内容自动加1(变为
0001H
);地址寄存器的内容(
0000H
)通过内部地址总线送到存储器,以存储器中地址译码电跟,使地址为0000H
的单元被选中;CPU使读控制线有效;
在读命令控制下被选中存储器单元的内容(此时应为74H)送到内部数据总线上,因为是取指阶段,所以该内容通过数据总线被送到指令寄存器。
4 多线程程序执行
为了提高CPU的使用率,换个角度想一下,既然不能减少一段程序的执行时间,就在同样的时间执行更多的程序,一个核执行一段程序,两个核就可以执行两段程序,于是多核CPU成为了现在的主流)。
所以裸机程序指令就在Flash(Flash memory)中存放,而数据就放在了RAM中(flash的写入次数有限制,同时它的速度和RAM还是差很多)。更广泛说,在单片机上RAM存放data段,bss段,堆栈段;ROM(EPROM,EEPROM,Flash等非易失性存储设备)存放代码,只读数据段。
本质上说,这和PC上程序都在RAM中存放是一样的,PC 上是操作系统规定了可读与可写,而单片机上是依靠不同的存储设备区分了可读与可写(当然现在的Flash是可读写的,如果Flash没有写入次数限制,速度又可以和RAM相差不多,单片机上是不是只要Flash就可以了呢(直接相当于PC上的DDRRAM)?这样成本也会比一个RAM,一个Flash低,更节省成本,对于生产商更划算)。
5 数据的存放与读取
对于单片机的程序执行时指令和数据的存放与读取,理解如下:
对单片机编程后,程序的代码段,data段,bss段,rodata段等都存放在Flash中。当单片机上电后,初始化汇编代码将data段,bss段,复制到RAM中,并建立好堆栈,开始调用程序的main函数。
之后,便有了程序存储器,和数据存储器之分,运行时从Flash(即指令存储器,代码存储器)中读取指令 ,从RAM中读取与写入数据。RAM存在的意义就在于速度更快。
无论是单片机也好,PC也罢,存在的存储器金字塔都是一致的,速度的因素,成本的限制导致了一级级更快的存储器的更快速度与更高的成本。应该说,对于它们的理解,就是存储器金字塔的理解。
对于单片机的程序执行时指令和数据的存放与读取,理解如下:
对单片机编程后,程序的代码段,data段,bss段,rodata段等都存放在Flash中。
当单片机上电后,初始化汇编代码将data段,bss段,复制到RAM中,并建立好堆栈,开始调用程序的main函数。
以后,便有了程序存储器,和数据存储器之分,运行时从Flash(即指令存储器,代码存储器)中读取指令 ,从RAM中读取与写入数据。
RAM存在的意义就在于速度更快。