Cortex-M4 基础知识

 

cortex-M4相关的一些基础知识

cortex-M4相关的一些基础知识

程序的产生过程

  • 问题:
    • gcc hello.c的过程是怎么样的?
    • Cortex-M的程序是怎么运行起来的?

程序的编译

对于 gcc hello.c -o hello 可以得到 hello 这个程序,但是这中间经过了下面的步骤:

流程 命令 说明
预处理 gcc -E hello.c -o hello.i 1. 处理#define, #if等命令
2. 引入#include的内容
3. 删除//注释
4. 其他
编译 1. gcc -S hello.i -o hello.s
2. cc1 hello.c -o hello.s
把c语言翻译成汇编语言
汇编 1. as hello.s -o hello.o
2. gcc -c hello.s -o hello.o
把汇编语言翻译成机器码
链接 ld -static crt1.o crti.o crtbeginT.o hello.o \
-start-group -lgcc -lgcc_en -lc -end-group
crtend.o rtn.o
把多个目标文件链接成一个hello文件

最后一步的链接,其实链接了多个目标文件,这些文件是怎么变成一个hello文件的?

在计算机早期,是通过纸带打孔来编程的,比如跳转到第5行指令,就是0b0001_01010b0001jump0b0101表第5行。但当修改编程之后,比如在第2行插入一行新指令后,这个0b0001_0101就要改成0b0001_0110,而且后面跟位置有关的指令都要修改。 为了解决这个问题,汇编语言就出现了。汇编语言用jump foo就可以实现跳转到foo位置,而且foo的位置可以通过计算机自动更新,这个更新程序中地址的过程就是重定位,就是重新确定名称位置。

随着代码变多和变复杂,我们会把实现某个功能的代码组织到一个函数中,而多个函数又被组织到一个.c文件(模块)中,最后一个复杂的程序再由多个模块组合而成。那么会出现一个模块会调用另一个模块中的函数或变量的情况,这个时候就需要确定本模块访问的另一个模块中符号(变量或函数)的地址。为了解决这个更复杂的问题,就出现了链接器。链接器就是把多个模块拼接到一个,同时把模块内访问其他模块的符号重定位。

目标文件的链接

一个可执行文件是由多个目标文件组合成的,那么这些目标文件自身是什么样的呢?

目标文件的结构

目标文件.o是由as汇编器得到的机器码,它的内容并不是按照.c的代码顺序依次编码得到的。而是根据代码的不同性质,分开放置的。从大的尺度看,.o文件分成了代码段和数据段。

为什么要这样组织呢?

  • 可以保护程序。
    • 代码段放到只读区,这样就不能修改它的内容
    • 嵌入式设备有ROM、RAM、Flash等存储设备,对于只读的代码和数据,可以放到ROM中;对于变动的内容,可以放到RAM中
  • 可以提高读取速度
    • 现代的CPU都有缓冲区,对于代码段可以一次缓冲多个指令;
    • 还可以根据代码、数据各自的特性,得到优化的指令缓冲区和数据缓冲区
  • 代码复用
    • 对于多个程序,它们的代码段都是相同的,所以可以公用一块代码段
    • 就像PLC的FB块,如果把数据和代码放在一起,就会占用大量的空间

所以,目标文件是按照段的方式组织的。

查看目标文件内容

指令 说明
objdump -s simple.o 查看文件所有段内容
objdump -d simple.o 反汇编二进制文件
objdump -x simple.o 详细打印文件内容

程序的运行

Cortex-M启动代码原理分析

ARM Cortex-M系列MCU的启动代码(使用汇编语言编程则不需要)主要做3件事情:

  1. 初始化并正确放置异常/中断向量表;
  2. 分散加载;
  3. 初始化C语言运行环境(初始化堆栈以及C Library、浮点等)。

Cortex-M复位之后,先进入厂商的BootRom进行最初级的硬件初始化、加密以及一些MCU差异化设置;完成之后,MCU交给用户代码,即用户的启动代码。

用户代码最重要的是设置主堆栈指针(MSP)和程序计数器(PC)的值。

Cortex-M默认把0x0处的数据设置为MSP的值,把0x4的数据设置为PC的值;即MSP = [0x0];PC = [0x4]。用户可以通过VTOR设置MSPPC映射的地址。

附录

查看目标文件内容

怎么查看目标文件的内容

  1. 查看目标文件所有段的内容

     ❯ objdump -s simple_section.o
        
     simple_section.o:     文件格式 elf64-x86-64
        
     Contents of section .text:
      0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
      0010 bf000000 00b80000 0000e800 00000090  ................
      0020 c9c35548 89e54883 ec10c745 f8010000  ..UH..H....E....
      0030 008b1500 0000008b 05000000 0001c28b  ................
      0040 45f801c2 8b45fc01 d089c7e8 00000000  E....E..........
      0050 8b45f8c9 c3                          .E...           
     Contents of section .data:
      0000 54000000 56000000                    T...V...        
     Contents of section FOOOO:
      0000 58000000                             X...            
     Contents of section .rodata:
      0000 25640a00                             %d..            
     Contents of section .comment:
      0000 00474343 3a202855 62756e74 7520352e  .GCC: (Ubuntu 5.
      0010 342e302d 36756275 6e747531 7e31362e  4.0-6ubuntu1~16.
      0020 30342e31 32292035 2e342e30 20323031  04.12) 5.4.0 201
      0030 36303630 3900                        60609.          
     Contents of section .eh_frame:
      0000 14000000 00000000 017a5200 01781001  .........zR..x..
      0010 1b0c0708 90010000 1c000000 1c000000  ................
      0020 00000000 22000000 00410e10 8602430d  ...."....A....C.
      0030 065d0c07 08000000 1c000000 3c000000  .]..........<...
      0040 00000000 33000000 00410e10 8602430d  ....3....A....C.
      0050 066e0c07 08000000                    .n......        
    

    我们看一下代码段.text的内容:

    段偏移量 数据 ascii字符
    0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
    0010 bf000000 00b80000 0000e800 00000090 ................

    .comment段内容:

    段偏移量 数据 ascii字符
    0000 00474343 3a202855 62756e74 7520352e .GCC: (Ubuntu 5.
    0010 342e302d 36756275 6e747531 7e31362e 4.0-6ubuntu1~16.
  2. 查看目标文件所有汇编指令(反汇编目标文件)

     ❯ objdump -d simple_section.o
        
     simple_section.o:     文件格式 elf64-x86-64
        
        
     Disassembly of section .text:
        
     0000000000000000 <func1>:
        0:   55                      push   %rbp
        1:   48 89 e5                mov    %rsp,%rbp
        4:   48 83 ec 10             sub    $0x10,%rsp
        8:   89 7d fc                mov    %edi,-0x4(%rbp)
        b:   8b 45 fc                mov    -0x4(%rbp),%eax
        e:   89 c6                   mov    %eax,%esi
       10:   bf 00 00 00 00          mov    $0x0,%edi
       15:   b8 00 00 00 00          mov    $0x0,%eax
       1a:   e8 00 00 00 00          callq  1f <func1+0x1f>
       1f:   90                      nop
       20:   c9                      leaveq 
       21:   c3                      retq   
        
     0000000000000022 <main>:
       22:   55                      push   %rbp
       23:   48 89 e5                mov    %rsp,%rbp
       26:   48 83 ec 10             sub    $0x10,%rsp
       2a:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
       31:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 37 <main+0x15>
       37:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 3d <main+0x1b>
       3d:   01 c2                   add    %eax,%edx
       3f:   8b 45 f8                mov    -0x8(%rbp),%eax
       42:   01 c2                   add    %eax,%edx
       44:   8b 45 fc                mov    -0x4(%rbp),%eax
       47:   01 d0                   add    %edx,%eax
       49:   89 c7                   mov    %eax,%edi
       4b:   e8 00 00 00 00          callq  50 <main+0x2e>
       50:   8b 45 f8                mov    -0x8(%rbp),%eax
       53:   c9                      leaveq 
       54:   c3                      retq   
    
  3. 详细打印文件内容

     ❯ objdump -x simple_section.o
        
     simple_section.o:     文件格式 elf64-x86-64
     simple_section.o
     体系结构:i386:x86-64, 标志 0x00000011:
     HAS_RELOC, HAS_SYMS
     起始地址 0x0000000000000000
        
     节:
     Idx Name          Size      VMA               LMA               File off  Algn
       0 .text         00000055  0000000000000000  0000000000000000  00000040  2**0
                       CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
       1 .data         00000008  0000000000000000  0000000000000000  00000098  2**2
                       CONTENTS, ALLOC, LOAD, DATA
       2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2
                       ALLOC
       3 FOOOO         00000004  0000000000000000  0000000000000000  000000a0  2**2
                       CONTENTS, ALLOC, LOAD, DATA
       4 .rodata       00000004  0000000000000000  0000000000000000  000000a4  2**0
                       CONTENTS, ALLOC, LOAD, READONLY, DATA
       5 .comment      00000036  0000000000000000  0000000000000000  000000a8  2**0
                       CONTENTS, READONLY
       6 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000de  2**0
                       CONTENTS, READONLY
       7 .eh_frame     00000058  0000000000000000  0000000000000000  000000e0  2**3
                       CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
     SYMBOL TABLE:
     0000000000000000 l    df *ABS*  0000000000000000 simple_section.c
     0000000000000000 l    d  .text  0000000000000000 .text
     0000000000000000 l    d  .data  0000000000000000 .data
     0000000000000000 l    d  .bss   0000000000000000 .bss
     0000000000000000 l    d  FOOOO  0000000000000000 FOOOO
     0000000000000000 l    d  .rodata        0000000000000000 .rodata
     0000000000000004 l     O .data  0000000000000004 static_var.1841
     0000000000000000 l     O .bss   0000000000000004 static_var2.1842
     0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
     0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
     0000000000000000 l    d  .comment       0000000000000000 .comment
     0000000000000000 g     O .data  0000000000000004 global_init_var
     0000000000000004       O *COM*  0000000000000004 global_uninit_var
     0000000000000000 g     O FOOOO  0000000000000004 global
     0000000000000000 g     F .text  0000000000000022 func1
     0000000000000000         *UND*  0000000000000000 printf
     0000000000000022 g     F .text  0000000000000033 main
        
        
     RELOCATION RECORDS FOR [.text]:
     OFFSET           TYPE              VALUE
     0000000000000011 R_X86_64_32       .rodata
     000000000000001b R_X86_64_PC32     printf-0x0000000000000004
     0000000000000033 R_X86_64_PC32     .data
     0000000000000039 R_X86_64_PC32     .bss-0x0000000000000004
     000000000000004c R_X86_64_PC32     func1-0x0000000000000004
        
        
     RELOCATION RECORDS FOR [.eh_frame]:
     OFFSET           TYPE              VALUE
     0000000000000020 R_X86_64_PC32     .text
     0000000000000040 R_X86_64_PC32     .text+0x0000000000000022
    

什么是VMA和LMA

  • VMA就是虚拟地址,也就是程序的运行地址;
  • LMA是装载地址,也就是程序在FLASH中放置的地址

链接脚本中的赋值

链接脚本中的=是指定变量虚拟地址VMA(运行时地址)的意ln,并不是c中给变量赋值的意思。比如链接脚本ld.lds内容

a = 0x3;

使用main.c的内容:

#include <stdio.h>
int a = 1000;

int main(void) {
  printf("&a = %p\n", &a);
}

通过gcc编译:

>>> gcc -Wall -o a-with-lds a.c a.lds

>>> ./a-with-lds
&a = 0x3

说明链接脚本中给a的赋值是规定a的虚拟地址(运行时地址)

AT关键字

输出section的LMA:默认情况下,LMA等于VMA,但可以通过关键字AT()指定LMA。

  • 用关键字AT()指定,括号内包含表达式,表达式的值用于设置LMA
  • 如果不用AT()关键字,那么可用AT>LMA_REGION表达式设置指定该section加载地址的范围,这个属性主要用于构件ROM境象

用AT的作用在于节省编译出来的elf文件或bin文件的大小,比如对于下面的链接脚本:

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")  
OUTPUT_ARCH(i386)  
SECTIONS  
{  
    .text 0x5000    : { *(.text) }  
    .data 0x8000    : { *(.data) }  
}  

在上面这个ld script中只定义了vma;根据ld的规则:如果没有用AT指令定义lma的话,那么lma默认等于vma。

这里为什么两个段(.data和.text)的vma不一样?试想在嵌入式系统中是不是会遇到这种情况,即Flash(Rom)空间较大,Ram空间相对较小,于是我们只希望让数据装载进Ram空间,代码就直接运行在Flash(Rom)中。比如Flash(Rom)的起始地址0x5000,Ram的起始地址0x8000,所以这两个段的vma就必须对应到相应Region的起始地址上。不然会怎么着?不是跑飞就是读写的数据找不到。

链接.o文件:

ld ./lma_vma.o -T ./lma.equal.vma.lds 

生成a.out可执行文件。

注意,这个a.out是‘可执行’的elf文件。对于bootloader或者firmware来说,一般是直接把一个binary文件 ‘burn’到板子上的。把elf文件剥离成一个binary文件,非常简单,一个objcopy便可搞定:

objcopy -O binary ./a.out  

我们先用ls -l看一下有什么问题:

# ls -lh ./a.out  
-rwxr-xr-x 1 root root 13K 11-04 20:55 ./a.out  

文件足足有13k大小。别忘了,我们的源程序只有一条指令和一个32位的字,并且是纯数据的bin文件,为什么有这么大? 借助于hexdump,真相一目了然:

# hexdump ./a.out  

0000000 01b8 0000 0000 0000 0000 0000 0000 0000  
0000010 0000 0000 0000 0000 0000 0000 0000 0000  
*  
0003000 9090 9090  

最开始01b8应该就是mov $1, $eax的instruction code。而0x3000位置的90909090显然就是我们定义在数据段的字了。因为链接器脚本中没有用AT指令专门为两个段指定lma,所以其lma与vma相等,两个段相差了0x3000 bytes的长度。.text段之前没有其他段了,所以最终的bin文件中一开始就是.text段的内容,虽然只有2个字节,但仍然要过0x3000bytes才是.data段。中间那些未知数就填0了。

这样有什么问题呢?因为我们知道0x8000已经是Ram了,难道我们要将全局数据 烧到一断电内容就消失的Ram中?并且,Flash(Rom)和 Ram之间相隔的0x3000 bytes不一定就对应实际的存储区域(比如也在Flash中),有可能根本就是hole。那么‘烧’这些0下去有可能会造成问题。

我们希望的结果是,烧写的data和text都在Flash(Rom)中,运行后再将data自搬运到Ram中。最好bin文件中两个段紧挨着,保持文件尽可能小的size。

下面的ld script在定义.data段时增加了AT指令来描述其lma,这样表示.data段的lma紧接在.text段的后面:

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")  
OUTPUT_ARCH(i386)  
SECTIONS  
{  
    .text 0x5000 : {  
        *(.text)  
    }  
    .data 0x8000 : AT(ADDR(.text) + SIZEOF(.text))  {  
        *(.data)  
    }  
}  

这次链接、objcopy后生成的a.out文件看一下:

# hexdump ./a.out  

0000000 01b8 0000 9000 9090 0090  
0000009  

只有9个字节大小,里面的内容正好是一条指令加上后面的0x90909090(指令后面的两个0x00是为了4字节对齐.data的pad),这个bin文件就可以放心的烧写到Flash(Rom)中去了。不过,将.data段搬运到Ram的代码还是得自己写的

那么如何写呢,可以看下面的例子,对于链接脚本

SECTIONS
{
  .text 0x1000 : { *(.text) _etext = . ; }

  .mdata 0x2000 : AT ( ADDR (.text) + SIZEOF (.text) ) { 
      _data = . ;  // 指定了_data符号/变量的虚拟地址
      *(.data); 
      _edata = . ; 
  }

  .bss 0x3000 :
  { _bstart = . ; *(.bss) *(COMMON) ; _bend = . ;}
}

使用AT指定了LMA,下面是对.data段的搬运:

extern char _etext, _data, _edata, _bstart, _bend;
char *src = &_etext; // src指向装载地址,同时也是VMA地址
char *dst = &_data; // 获取_data变量的虚拟地址
                    // 链接脚本中_data=.的值c语言要用&_data取
                    // 因为链接脚本只能给地址,不能赋数值

/* ROM has data at end of text; copy it. */
while (dst < &_edata) {
  *dst++ = *src++;
}

/* Zero bss */
for (dst = &_bstart; dst< &_bend; dst++)
  *dst = 0;
  • char *src = &_etext,源地址是LMA也是VMA
  • char *dst = &_data
    • 因为链接脚本中用了AT指令,使能LMA与VMA不同了
      • LMA变成了 ADDR (.text) + SIZEOF (.text)
      • VMA指定成了 0x2000
    • dst指向链接脚本_data=.指定的虚拟地址
  • *dst ++ = *src++把LMA指定的data段复制到VMA指定的data段

参考《GNU-ld链接脚本浅析