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_0101
。0b0001
表jump
,0b0101
表第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件事情:
- 初始化并正确放置异常/中断向量表;
- 分散加载;
- 初始化C语言运行环境(初始化堆栈以及C Library、浮点等)。
Cortex-M
复位之后,先进入厂商的BootRom
进行最初级的硬件初始化、加密以及一些MCU差异化设置;完成之后,MCU交给用户代码,即用户的启动代码。
用户代码最重要的是设置主堆栈指针(MSP)和程序计数器(PC)的值。
Cortex-M
默认把0x0
处的数据设置为MSP的值,把0x4
的数据设置为PC的值;即MSP = [0x0];PC = [0x4]
。用户可以通过VTOR
设置MSP
和PC
映射的地址。
附录
查看目标文件内容
怎么查看目标文件的内容
-
查看目标文件所有段的内容
❯ 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.
-
查看目标文件所有汇编指令(反汇编目标文件)
❯ 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
-
详细打印文件内容
❯ 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个字节,但仍然要过0x3000
bytes才是.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也是VMAchar *dst = &_data
- 因为链接脚本中用了
AT
指令,使能LMA与VMA不同了- LMA变成了
ADDR (.text) + SIZEOF (.text)
- VMA指定成了
0x2000
- LMA变成了
- 让
dst
指向链接脚本_data=.
指定的虚拟地址
- 因为链接脚本中用了
*dst ++ = *src++
把LMA指定的data段复制到VMA指定的data段
参考《GNU-ld链接脚本浅析》