单片机运行原理详解:bss_data_text段、.ld链接文件和startup.S启动文件介绍

一、bss_data_text段介绍

1.简介

  • 一个程序本质上都是由 BSS 段、DATA段、TEXT段三个组成的。下面主要分编译时和运行时分别对 data段 bss段 text段 堆 栈作一简要说明。
  • C语言程序与编译后的目标文件有如下的对应关系:

  •  2 bss段

    bss段(bss segment,bss是英文Block Started by Symbol的简称),bss段属于静态内存分配。

    通常是指用来存放程序中未初始化(或初始化为0)的全局变量的一块内存区域。
    运行时所需要的BSS段大小记录在目标文件中,但BSS段并不占据目标文件的任何空间。

    3 data段

    数据段(data segment),数据段属于静态内存分配。
    通常是指用来存放程序中已初始化(非零)的非const的全局变量的一块内存区域。
    注:const全局变量一般放到了rodata段,初始化为零的全局变量可能被编译器优化到 bss段

    4 text段

    代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。
    代码段大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。
    在代码段中,可能包含一些只读的常数变量,例如字符串常量等。

    注:1.text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。
        2.text段一般也包含rodata段,text段可以存放到flash中;data段也放到flash中,运行时由flash中加载到SRAM;bss段在运行时系统创建,存放到RAM中
     

     5 程序运行时的概念介绍

    5.1 堆

    堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
    当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);
    当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

    5.2 栈

    栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。
    除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
    由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。
    从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

    5.3 编译生成的可执行文件在内存中的布局

    5.4 案例介绍

    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类型的全局变量是即占文件空间,又占用运行时内存空间的

    5.5 stm32 单片机编译后的内存布局介绍

    二、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’后面的指定地址方式不支持,因此忽略掉了。

    四、单片机运行原理

    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单元中已存放74H0001H单元中已存放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存在的意义就在于速度更快。

    物联沃分享整理
    物联沃-IOTWORD物联网 » 单片机运行原理详解:bss_data_text段、.ld链接文件和startup.S启动文件介绍

    发表评论