前言
资料
《ARM Cortex-M0权威指南》 《程序员的自我修养——链接、装载与库》 GNU Assembler官网、博客 Cortex-M0官网
什么是启动代码
工作需要,用到arm cortex-m0和m3,系统上电复位后、到进入C程序之前,需要一段启动代码,用于系统最基本的初始化:初始化堆栈指针、设置页表、操作 ARM的协处理器等。这段启动代码就是大名鼎鼎的“boot脚本”,启动代码执行的过程称为“bootloader",这两种称呼也可以混着用。
这段启动代码通常是汇编语言撰写。
CPU与指令集
CPU分为多种指令集,例如ARM、MIPS、RISC-V,每种指令集又分成多种子集,例如ARM指令集包括:32位ARM指令集、16位Thumb指令集、32位Thumb2指令集等。每款CPU具体用什么指令集需要查询各自的芯片手册。每个指令集对应自己的汇编语言,例如ARM汇编(生态成熟)、MIPS汇编、RISC-V汇编(这个开源),不同的汇编语言,汇编命令不同,例如,cortex-m0对应56条汇编命令:
汇编命令与汇编器
同一种汇编语言,用不同的开发工具供应商(例如GNU、ARM)提供的汇编工具语法又不同,例如ARM汇编器下的一段针对Cortex-M0的代码:
NVIC_IRQ_SETEN EQU 0xE000E100
NVIC_IRQ0_ENABLE EQU 0x1
...
LDR R0, = NVIC_IRQ_SETEN ;将0xE000E100放到R0
;此处LDR为伪指令
MOVS R1, #NVIC_IRQ0_ENABLE' ;将立即数放到寄存器R1
STR R1, [R0] ;将0x1存到0xE000E100,这样会使能外部中断IRQ#0
对应的,基于GNU工具链汇编器的相同功能代码写作:
.equ NVIC_IRQ_SETEN 0xE000E100
.equ NVIC_IRQ0_ENABLE 0x1
...
LDR R0, = NVIC_IRQ_SETEN /*将0xE000E100放到R0*/
/*此处LDR为伪指令*/
MOVS R1, #NVIC_IRQ0_ENABLE' /*将立即数放到寄存器R1*/
STR R1, [R0] /*将0x1存到0xE000E100,这样会使能外部中断IRQ#0*/
可见,GNU工具的操作代码和操作数的语法同ARM汇编器的一致,但标号、注释等语法不同(例如.equ )。
小结
综上所述,为了写好Cortex-M启动代码,必须学会GNU汇编器语法、Cortex-M指令集。
0. GNU汇编器简介
GNU汇编器简称as ,在我下载的工具链中,它的文件名是“arm-none-eabi-as.exe",其中“eabi”代表用于没有OS的开发。 下图为产生CPU可执行映像的开发流程,可见我们要用到汇编器(assembler)、C编译器(compiler)、链接器(linker),而在项目实际中,我们采用的是GNU交叉编译工具链。所谓工具链,就是在bin中提供了GNU ARM汇编器、GNU C编译器和GNU链接器等等需要的工具。
本文,仅学习GNU汇编器语法,不包括Cortex-M0汇编命令、C编译器、链接器内容,如有时间,后续再单独开设博客。
示例:GNU Assembler是TIGCC的一部分,TIGCC可以看作TI+GCC,也就是用于一系列TI产品的允许同时编译GNU汇编代码、C代码的软件开发环境。
1. 命令行
2. 输入输出
3. 基本语法
正如其他汇编器的做法,GNU汇编器采用机器无关(machine-independent)的语法设计。
instruction 指令 directive 汇编指示 pseudo-instruction 伪指令 pseudo-operation 伪操作 operator 操作符
3.1 预处理
as 预处理器的作用有且仅有:
- 调整空格
保留行首的一个空格或者Tab键,其他位置保留一个空格 - 删除注释
- 变换字符常量
将字符常量(character constant)转换为合适的数值
与你常用的C预处理器不同,as预处理器没有诸如展开宏、include文件等等功能。 如果你需要GNU C compiler进行这种“CPP”风格的预处理,那么就把汇编文件的后缀改成大写的.S ; 如果你需要include问加你,那么就在汇编文件中使用.include 汇编指示;
-f 选项和汇编文件内部的#NO_APP 、#APP ,可以控制空格和注释的保留与否。
3.2 空格
英文中whitespace、blank、tab、space都被翻译成中文,其实从中文没法分清,好在GNU汇编器中这些都跟一个空格键功能相同。至于你用空格还是Tab,主要是方便人类阅读,在GNU汇编器的语法中,他们并无不同。
3.3 注释
GNU汇编器的注释有两种:
- /*xxx*/
这种风格的注释可以跨行,也可以单行 - #后跟非数字
# 后跟数字在GNU汇编器中另有作用,后跟非数字则看作注释; 强烈不推荐这种注释风格,未来这样用法也可能被丢弃;
3.4 符号
symbol由字母、数字、下划线、点号、美元符号构成,区分大小写,不以数字打头。
3.5 语句
statement以换行符\n 或者冒号: 结尾,每个汇编文件的最后一行必须是空行。
语句开头可以有0个或者多个标签(label),后面可以再跟上决定这条语句类型的关键symbol。关键symbol决定了该语句剩余部分的语法。
如果关键symbol以点号. 打头,那么这条语句是汇编指示(directive); 如果关键symbol后跟冒号: ,那么这个符号称为标签(label),注意label和冒号之间不允许有空格; 如果关键symbol以字母打头,那么这条语句是汇编语言的指令(instruction),包括操作符和若干操作数;
label: .directive followed by something
another_label: # This is an empty statement.
instruction operand_1, operand_2, ...
3.6 常量
constant分为字符常量和数字常量,常见形如:
.byte 74, 0112, 092, 0x4A, 0X4a, 'J, '\J # All the same value.
.ascii "Ring the bell\7" # A string constant.
.octa 0x123456789abcdef0123456789ABCDEF0 # A bignum.
.float 0f-314159265358979323846264338327\
95028841971.693993751E-40 # - pi, a flonum.
3.6.1 字符常量
字符常量又分为双引号"" 内的字符串字面值(string literals)和单撇号' 后的单字符,支持转义字符。 为了兼容Unix系统,GNU汇编器中\008表示010,\009表示011,所以0112 和092 都表示十进制的74。
3.6.2 数字常量
数字常量支持三种:整数(Integer)对应C语言的整型,大数(Bignum)占内存超过32bit,浮点数(Flonum)对应C语言中的float。
4. 段和重定位
汇编阶段的“段”指section,描述section的结构叫段表section table; 装载阶段的“段”指segment,描述segment的结构叫程序头program header。
粗略的说,section就是一个连续的地址范围,所有此间的数据都被同等看待、同样处理。 链接器ld 读入若干目标文件(object file,在linux下后缀.o ,在window下后缀.obj ),进行一些处理后输出可执行文件(在linux下是ELF格式后缀.elf ,在window下是PE/COFF格式后缀.exe )。链接又分静态链接和动态链接,所做的处理有:分配空间和地址、解析符号、重定位等。
as 汇编器和C编译器输出的目标文件又可称为partial program,partial指它还不能执行,因为它的首地址是0,也就是还没有分配地址,program指出它是静态文件,而不是动态的进程。
链接器ld 将程序中的字节块以section为单位原封不动搬移到运行时的真实位置,不改变字节块内部的大小或者顺序。确定section真实位置的过程称为重定位。有些section专用于汇编阶段,ld 对它们不做处理。
用as 汇编器生成的目标文件至少包含三个段:text、data、bss,不过段内可以是空的。在目标文件中,代码段text的首地址是0,随后是数据段data,再后是bss段。
可以用.section 汇编指示指定as 生成的段的名字。
as 必须在目标文件中写清楚重定位信息,这样ld 才知道重定位时哪些数据要修改、要怎么修改。要执行重定位,每次在object文件中提到一个地址,ld 必须知道:
- 在目标文件的这个地址引用的开始在哪
- 这个引用有多长(以字节为单位)
- 这个地址指的是哪一部分
- 地址的引用是“程序计数器相对”的吗
事实上,每一个as 使用的地址都表达为(section)+(offset into section),下文中用{secname N} 表示secname段内偏移N字节。
as 中还有一个重要概念叫做“绝对段”,也就是ld 在链接是不会修改absolute段的地址。例如,{absolute 239} 表示在重定位到运行时阶段是,地址真的就是239。
任何在汇编时section未知的地址都被定义为{undefined U} -其中U是稍后填写的。生成一个未定义地址的唯一方法就是提到一个未定义的符号,例如对具名common块的引用。
对于C/C++语言来说,有些符号的定义可以被称为弱符号,例如未初始化的全局变量。与强符号相比,链接器允许一个弱符号在多个目标文件中存在,并在实际链接时选择占用空间最大的那个,这就叫common block机制。类似的概念还有弱引用,这些特性在动态链接中非常重要。
4.1 链接器段
ld 只处理4种段:
- data段
text代码段一般只读,不修改,进程间共享;data数据段每个进程独有一份,可写; - bss段
bss段记录程序中的未经初始化的全局变量和静态变量,目标文件中的bss段不占内存,链接后的bss段占用内存,且全部置零。发明bss段就是为了减少目标文件的大小,消除不必要的0的存储; - absolute段
绝对段不允许重定位(unrelocatable),链接器必须按照原地址放置; - undefined段
这个段是所有找不到的地址引用的全称;
4.2 汇编器内置段
有些段只在汇编阶段有意义,运行时没有意义,也就不需要链接。
4.3 子段
你可能有多组数据,她们在汇编源码中并不连续,但是你希望它们在目标文件中连续,这时as 提供了subsection机制。一个section可以有0-4096个subsection,例如:
.text 0 # The default subsection is text 0 anyway.
.ascii "This lives in the first text subsection. *"
.text 1
.ascii "But this lives in the second text subsection."
.data 0
.ascii "This lives in the data section,"
.ascii "in the first data subsection."
.text 0
.ascii "This lives in the first text section,"
.ascii "immediately following the asterisk (*)."
4.4 bss段
.lcomm 和.comm 汇编指示、.section 符号可以修改bss段;
5. 符号
symbol是一个核心概念:程序员用symbol命名事物,链接器用symbol连接目标文件,调试器用symbol定位问题。
5.1 标签
label后紧跟冒号,用于表示活动位置计数器(active location counter)的当前值,重复label会告警并覆盖。
5.2 符号赋值
可以用等号表达式= 或者汇编指示.set 给symbol赋任意值。
5.3 符号名
symbol由字母、数字、下划线、点号、美元符号构成,区分大小写,不以数字打头。 局部符号名 为了快速打标签,支持在作用域内使用local symbol,写符号用N: 其中N为正整数,用Nb 指代前一个数值相同的标签,用Nf 指代下一个数值相同的标签,b-backward, f-forward,例如:
1: jra 1f
2: jra 1b
1: jra 2f
2: jra 1b
等效于(jra是跳转伪指令):
label_1: jra label_3
label_2: jra label_1
label_3: jra label_4
label_4: jra label_3
美元局部标签 as 支持一种更加局部的局部标签,称为dollar label(以$ 结尾,而不是: )。一旦定义了非局部标签,这些dollar label就变成未定义的了,生命周期很短。
5.4 特殊点号
. 点号特殊symbol表示as 汇编的当前地址。给. 点号赋值,等同于.org 汇编指示。
5.5 符号特性
每个symbol除了名字以外,还有Value和Type两个属性,根据输出格式不同,symbol还可以有其他附加属性。 Value symbol的Value一般是32bit,text、data、bss和absolute段的symbol值是从段起始为止到标签的偏移地址。当然了,text、data、bss段的symbol值在链接后会改变,因为不同目标文件的同名段的合并和重定位过程地址会被修正。 Type symbol的type属性包含重定位(节)信息、任何标志设置,以及(可选地)用于连接器和调试器的其他信息。确切的格式取决于所使用的目标代码输出格式。
6. 表达式
Expression指定地址或数值,分为空表达式、整型表达式,整型表达式是由操作符(operator)分割的1或多个argument。 argument argument可以是symbol、数字、子表达式。在其他情况下,argument有时被称为“算术操作数”。在本手册中,为了避免与机器语言的“指令操作数”混淆,我们使用术语“argument”仅指表达式的部分,保留“operand”一词仅指机器指令操作数。
操作符 操作符就是算术函数,例如: 二进制补码的非号- ,按位取反~ ; 高优先级乘号* 、除号/ 、余号% 、左移< << 、右移> >> ; 中优先级按位或| 、按位与& 、按位异或^ 、按位或非! ; 低优先级加、减、判同== 、判不同<> 、小于、大于、大于等于、小于等于、逻辑与&& 、逻辑或|| ;
7. 汇编指示
所有汇编指示都以点号. 打头,大约80个。太多了,不介绍了,下面分析一个实例:
7.1 举个栗子
.syntax unified
.arch armv6-m
.section .stack
.align 3
.equ Stack_Size, 0x00000400
.globl __StackTop
.globl __StackLimit
__StackLimit:
.space Stack_Size
.size __StackLimit, . - __StackLimit
__StackTop:
.size __StackTop, . - __StackTop
.section .heap
.align 3
.equ Heap_Size, 0x00000400
.globl __HeapBase
.globl __HeapLimit
__HeapBase:
.space Heap_Size
.size __HeapBase, . - __HeapBase
__HeapLimit:
.size __HeapLimit, . - __HeapLimit
.section .isr_vector
.align 2
.globl __isr_vector
__isr_vector:
.word __StackTop
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word 0
.word 0
.word PendSV_Handler
.word SysTick_Handler
.word UART0_Handler
.word GPIO0_Handler
.word GPIO1_Handler
.word QSPI0_Handler
.word DAP_QSPI0_Handler
.word DAP_SPI0_Handler
.word DAP_QSPI_XIP_Handler
.word DAPLinkFittedn
.word Unused_IRQ8
.word Unused_IRQ9
.word Unused_IRQ10
.word Unused_IRQ11
.word Unused_IRQ12
.word Unused_IRQ13
.word Unused_IRQ14
.word Unused_IRQ15
.word Unused_IRQ16
.word Unused_IRQ17
.word Unused_IRQ18
.word Unused_IRQ19
.word Unused_IRQ20
.word Unused_IRQ21
.word Unused_IRQ22
.word Unused_IRQ23
.word Unused_IRQ24
.word Unused_IRQ25
.word Unused_IRQ26
.word Unused_IRQ27
.word Unused_IRQ28
.word Unused_IRQ29
.word Unused_IRQ30
.word Unused_IRQ31
.size __isr_vector, . - __isr_vector
.text
.thumb
.thumb_func
.align 2
.globl Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr r1, =__etext
ldr r2, =__data_start__
ldr r3, =__data_end__
.flash_to_ram_loop:
ldr r0, [r1]
str r0, [r2]
adds r1, r1, #4
adds r2, r2, #4
cmp r2, r3
bne .flash_to_ram_loop
ldr r0, =0
ldr r1, =__bss_start__
ldr r2, =__bss_end__
.clear_bss_loop:
str r0, [r1]
adds r1, r1, #4
cmp r1, r2
bne .clear_bss_loop
ldr r0, =SystemInit
blx r0
ldr r0, =main
blx r0
exit_loop:
nop
b exit_loop
.pool
.size Reset_Handler, . - Reset_Handler
.macro def_default_handler handler_name
.align 1
.thumb_func
.weak \handler_name
.type \handler_name, %function
\handler_name :
b .
.size \handler_name, . - \handler_name
.endm
def_default_handler NMI_Handler
def_default_handler HardFault_Handler
def_default_handler SVC_Handler
def_default_handler PendSV_Handler
def_default_handler SysTick_Handler
def_default_handler UART0_Handler
def_default_handler GPIO0_Handler
def_default_handler GPIO1_Handler
def_default_handler QSPI0_Handler
def_default_handler DAP_QSPI0_Handler
def_default_handler DAP_SPI0_Handler
def_default_handler DAP_QSPI_XIP_Handler
def_default_handler DAPLinkFittedn
def_default_handler Unused_IRQ8
def_default_handler Unused_IRQ9
def_default_handler Unused_IRQ10
def_default_handler Unused_IRQ11
def_default_handler Unused_IRQ12
def_default_handler Unused_IRQ13
def_default_handler Unused_IRQ14
def_default_handler Unused_IRQ15
def_default_handler Unused_IRQ16
def_default_handler Unused_IRQ17
def_default_handler Unused_IRQ18
def_default_handler Unused_IRQ19
def_default_handler Unused_IRQ20
def_default_handler Unused_IRQ21
def_default_handler Unused_IRQ22
def_default_handler Unused_IRQ23
def_default_handler Unused_IRQ24
def_default_handler Unused_IRQ25
def_default_handler Unused_IRQ26
def_default_handler Unused_IRQ27
def_default_handler Unused_IRQ28
def_default_handler Unused_IRQ29
def_default_handler Unused_IRQ30
def_default_handler Unused_IRQ31
7.2 编译它
用arm-none-eabi-gcc -c -g -O0 -Wall -mthumb -o start.o start.s 编译后产生目标文件start.o,用readelf解析文件头和段表如下:
$ readelf -h start.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 5288 (bytes into file)
Flags: 0x5000000, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 21
Section header string table index: 20
$ readelf -S start.o
There are 21 section headers, starting at offset 0x14a8:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000098 00 AX 0 0 4
[ 2] .rel.text REL 00000000 00123c 000038 08 I 18 1 4
[ 3] .data PROGBITS 00000000 0000cc 000000 00 WA 0 0 1
[ 4] .bss NOBITS 00000000 0000cc 000000 00 WA 0 0 1
[ 5] .stack PROGBITS 00000000 0000d0 000400 00 0 0 8
[ 6] .heap PROGBITS 00000000 0004d0 000400 00 0 0 8
[ 7] .isr_vector PROGBITS 00000000 0008d0 0000c0 00 0 0 4
[ 8] .rel.isr_vector REL 00000000 001274 000138 08 I 18 7 4
[ 9] .debug_line PROGBITS 00000000 000990 000086 00 0 0 1
[10] .rel.debug_line REL 00000000 0013ac 000008 08 I 18 9 4
[11] .debug_info PROGBITS 00000000 000a16 000026 00 0 0 1
[12] .rel.debug_info REL 00000000 0013b4 000038 08 I 18 11 4
[13] .debug_abbrev PROGBITS 00000000 000a3c 000014 00 0 0 1
[14] .debug_aranges PROGBITS 00000000 000a50 000020 00 0 0 8
[15] .rel.debug_arange REL 00000000 0013ec 000010 08 I 18 14 4
[16] .debug_str PROGBITS 00000000 000a70 00003b 01 MS 0 0 1
[17] .ARM.attributes ARM_ATTRIBUTES 00000000 000aab 00001b 00 0 0 1
[18] .symtab SYMTAB 00000000 000ac8 0004a0 10 19 24 4
[19] .strtab STRTAB 00000000 000f68 0002d1 00 0 0 1
[20] .shstrtab STRTAB 00000000 0013fc 0000a9 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
7.3 语法规则和架构
.syntax unified 命令是ARM架构独有的命令,用法很简单 .syntax [unified | divided] 。 作用是在汇编ARM汇编源时,指定按照怎样的语法规则进行汇编。 如果在编写汇编语言时不使用该命令指定语法规则,那么默认采用 .syntax divided ,此时使用旧的汇编风格。
.arch armv6-m 命令指定cortex-m0 CPU的架构。
7.4 栈
.section .stack
.align 3
.equ Stack_Size, 0x00000400
.globl __StackTop
.globl __StackLimit
__StackLimit:
.space Stack_Size
.size __StackLimit, . - __StackLimit
__StackTop:
.size __StackTop, . - __StackTop
.section汇编指示的语法是: .section name[, "flags"] or .section name[, subsegment] 。双引号内的是权限标识,例如可读、可执行等。 这一句定义了一个名为“.stack”的段。
.align汇编指示的语法是.align alignment[, [fill][, max]] ,用来指定数据的对齐方式。 这一句使位置计数器前进,直到它是2的3次方(也就是8)的倍数。
.equ汇编指示的语法是.equ symbol, expression ,用表达式给符号赋值。 这一句定义了一个符号Stack_Size,并赋值为0x0400。
.global汇编指示的语法是.global symbol ,使得链接器可以看到指定符号。
.space汇编指示的语法是.space size[, fill] ,分配size字节的数据空间,并填充其值为fill,缺省填0。
.size汇编指示的语法是.size expression ,设定指定符号的大小。. 表示当前地址,减去__StackLimit符号的地址为整个__StackLimit函数的大小。
从readelf -S的结果可以看到,目标文件中的确有一个叫做“.stack"的段,而且size是0x0400。
7.5 堆
.section .heap
.align 3
.equ Heap_Size, 0x00000400
.globl __HeapBase
.globl __HeapLimit
__HeapBase:
.space Heap_Size
.size __HeapBase, . - __HeapBase
__HeapLimit:
.size __HeapLimit, . - __HeapLimit
从readelf -S的结果可以看到,目标文件中的确有一个叫做“.heap"的段,而且size是0x0400。
7.6 向量表
.section .isr_vector
.align 2
.globl __isr_vector
__isr_vector:
.word __StackTop
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler
.word 0
.word 0
.word PendSV_Handler
.word SysTick_Handler
.word UART0_Handler
.word GPIO0_Handler
.word GPIO1_Handler
.word QSPI0_Handler
.word DAP_QSPI0_Handler
.word DAP_SPI0_Handler
.word DAP_QSPI_XIP_Handler
.word DAPLinkFittedn
.word Unused_IRQ8
.word Unused_IRQ9
.word Unused_IRQ10
.word Unused_IRQ11
.word Unused_IRQ12
.word Unused_IRQ13
.word Unused_IRQ14
.word Unused_IRQ15
.word Unused_IRQ16
.word Unused_IRQ17
.word Unused_IRQ18
.word Unused_IRQ19
.word Unused_IRQ20
.word Unused_IRQ21
.word Unused_IRQ22
.word Unused_IRQ23
.word Unused_IRQ24
.word Unused_IRQ25
.word Unused_IRQ26
.word Unused_IRQ27
.word Unused_IRQ28
.word Unused_IRQ29
.word Unused_IRQ30
.word Unused_IRQ31
.size __isr_vector, . - __isr_vector
首先定义一个名为.isr_vector 的段,对齐方式为4字节,定义了一个链接器可见的全局变量__isr_vector 。 .word汇编指示的语法为.word expressions ,插入一个4字节的数据。 文中将一系列数值塞入向量表,从readelf -S的结果可以看到,目标文件中的确有一个叫做“.isr_vector"的段,而且size是0x00c0=192字节,正好是塞进去的48个4字节数据。
7.7 代码段
.text
.thumb
.thumb_func
.align 2
.globl Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
ldr r1, =__etext
ldr r2, =__data_start__
ldr r3, =__data_end__
.text汇编指示的语法是.text [subsection] ,告诉汇编器as 把下列内容放入代码段; .thumb等同于.code 16, 表明使用Thumb指令; .thumb_func用来指明一个函数是thumb指令集的函数;
总结
提示:这里对文章进行总结: 例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。
|