公众号【嵌入式er笔记】持续记录和分享C/C++、Linux、ARM、Android、IoT等技术相关知识,以及职场、生活经验和感悟。
分类: LINUX
2013-03-29 13:55:42
原文地址:GNU ARM汇编语法入门 作者:xu48169172
GNU汇编器是GNU工具套件之一,其作用是把ARM汇编源代码转换成二进制对象文件。该汇编器的详细资料请参见GNU Assembler Manual,本文是该手册的摘要。
例子和模板文件
Examples 文件夹和他的子文件夹包含很多汇编语言程序例子,你可以学习它们。
Examples 有一个子文件是templates。在你开始写程序之前,强烈建议你使用那个文件夹提供的模板做为起点。特别的,template.s 文件应该在你所有的ARM程序中使用。在去除大部分该文件的注释后,内容如下:
.text ; 以下为可执行代码
_start: .global _start ; "_start" 是连接器所必须的
.global main ; "main" 是主程序
b main ; 跳转到主程序
main: ; "main" 程序入口
; 这里添加你自己的程序
mov pc,lr ; 返回
.end
调用汇编器
你可以使用arm-elf-as工具来编译任何ARM汇编代码,该工具的调用方式如下:
arm-elf-as -marm7tdmi --gdwarf2 -o filename.o filename.s
其中:
Filename 需要编译的文件。
-marm7tdmi 告诉GNU汇编器你的CPU内型是ARM7TDMI(ARMv4T 版本)。
--gdwarf2 让汇编器附带输出DEBUG信息
GNU Assembler Reference的第一章,第二章以及8.4节列出了其他选项。对于大的工程来说,必须按模块划分成多个源文件。每个源文件(后缀为.s)的编译方法和上面所示的单个文件的方法一样。
当你已经把源文件编译成二进制目标文件(后缀名.o)后,可以使用GNU链接器生成最终的可执行文件(后缀.elf),调用方式如下:
arm-elf-ld -o filename.elf filename.o
一次又一次的输入命令行将十分枯燥(尽管Unix Shell允许你使用方向键的向上键来显示上一次的命令),为了解决上述问题,你可以修改GNU提供的Makefile模板来达到你自己的目的(该模板的路径为:examples/templates/Makefile.template-asm)。一旦修改完该文件(并且重新命名该文件:Makefile),接下来所要做的就是输入命令:
Make
汇编语言语法
GNU汇编器支持多种架构的CPU而不仅仅是ARM。正因为如此,它的汇编语法和其他的ARM汇编器有细微的区别。GNU汇编器对于其支持的45种CPU架构采用相同的汇编语法。汇编源文件由声明(每行一个)组成,声明格式如下,声明的每个部分都是可选的。
label: instruction ; comment
label可以确定其所在位置的程序计数器(pc)值,然后你就可以使用它。例子:在分支或者load和store指令中的目标。一个标号可以由任何有效的字符和冒号组成,所谓有效的字符包含以下字符:字母A-Z,a-z,数字0-9,符号"_",".","$"。但是要注意的是标号不能以数字开头(更多信息请看GNU Assembler Reference的3.4和5.3节)。
Comment以";"开头,不管是什么内容。除了在字符串中出现的分号,系统将忽律整行。C语言风格的注释(/*……*/)也同样被支持,同样的你也可以使用"@"来代替分号";"。
Instruction区域是你的程序的主要组成:你可以使用任何你想用的ARM汇编语言指令。 同样包括所谓的伪指令或者汇编命令(用来告诉汇编器自己干某些特殊的事)。这些汇编命令将在下面做详细介绍。
汇编命令
所有的汇编命令都是以"."开头。这些命令在GNU Assembler Reference的第七章做了更加详细的介绍。下面所列出的这些命令(按字母排列)是在程序中最常用的。
.align
插入0到3个字节的0x00,,这样下一个位置将是4字节的整数倍。特别的,ARM微控制器总是按字(4字节)读取数据。下面的例子中,输出对象文件中将被插入8字节的内容,假设第一行代码的位置是4字节的整数倍。
.byte 0x55 ; 插入1个字节 0x55
.align ; 顺次插入3个字节: 0x00 0x00 0x00
.word 0xAA55EE11 ; 插入内容 0x11 0xEE 0x55 0xAA (小端模式)
顺便说一下,前缀0x表示这个数是个16进制数,这个文档后面的表达式部分将对此做详细介绍。这个汇编命令还可以带选项,不过这儿不做介绍。如果你想使用它们,建议你用.balign命令代替。更多信息请看GNU Assembler Reference的7.3。
.ascii "string" …
在对象文件中按照指定的方法插入数字字符串,该字符串末尾没有NUL字符。该命令一次可以插入多个字符串,字符串之间用","分隔。下面的例子在对象文件中插入3个字节长的字符串。
.ascii "JNZ" ; 插入3个字节: 0x4A 0x4E 0x5A
.asciz "string" …
和.ascii相似,只是生成的字符串以NUL(0x00)结尾。下面的例子在对象文件中插入4个字节长的字符串。
.ascii "JNZ" ; 插入4个字节: 0x4A 0x4E 0x5A 0x00
.byte expression …
在对象文件中插入一个字节,内容为expression的值。可以一次插入多个表达式,以","分隔。下面的例子中共向对象文件中插入了5个字节内容。
.byte 64, 'A' ; 插入2字节 0x40 0x41
.byte 0x42 ; 插入字节 0x42
.byte 0b1000011, 0104 ; 插入2字节 0x43 0x44
注意以0x和0X开头的数字表示该数字是十六进制,以0b和0B开头的数字表示该数字是二进制,以0开头的数字表示该数字是八进制。这个文档后面的表达式部分将对此做详细介绍,同时请看.hword 和 .word 汇编命令部分。
.data
选择该命令以下的内容位于最终可执行文件的数据段。所有的可执行程序至少包含两个段,.我们称之为.text和.data段。(缺一段)
技术上来讲,意味着只有可执行的代码才能出现在.text段(尽管只读恒量也同样适合),可读写的数据才能出现在.data段,不过对于GNU汇编器来说,这并不是强制的。GNU Assembler Reference的第四章将深入讨论这个主题。
.end
标记一个源代码文件的结尾。所有的在这个命令之后的内容将被汇编器忽略。这个命令完全可选,但强烈建议使用。
.equ symbol, expression
设置symbol的值为expression。这个汇编指令和.set,"="完全一样。下面的例子admas的值都是42:
.equ adams, (5 * 8) + 2
.set adams, 0x2A
adams = 0b00101010
.extern symbol
声明symbol的定义在其他源文件中。这个命令是可选的,因为汇编器对于任何未定义的标号都默认为是外部定义的。不过仍然建议使用。
.global symbol
声明symbol是全局可见的。标号_start是GNU链接器用来指定第一个要执行指令所必须的,同样的是全局可见的(并且只能出现在一个模块中)。
.hword expression …
.2byte expression …
在对象文件中插入半字(16位),内容为expression的值。可以同时插入多个,以","分隔。这个两个命令是一样的。下面的例子在对象文件中插入8个字节内容。
.hword 0xAA55, 12345 ; 插入4字节:0x55 0xAA 0x39 0x30
.2byte 0x55AA, -1 ; 插入4字节 0xAA 0x55 0xFF 0xFF
; 假设小端模式
注意ARM微控制器在16位地址边界上,一次总是读取16位数据,换句话说,它不能一次读取16位数据在一个奇数地址上。请参考.align命令寻找解决办法。同时参考.byte和.word命令。
.include "filename"
在当前源文件中插入filename的内容。这和C语言的#include是同样的效果,所以通过这个命令也可以包含定义变量的头文件。顺便提一下,注意被包含文件中是否有.end:汇编器将忽略后面的所有的内容。
.ltorg
插入存储恒量的文字池。文字池是ARM汇编语言的伪指令 ldr = 和 adrl所使用。这个汇编指令用的很少,因为GNU汇编器会精确的计算出什么时候,什么地方放置文字池。尽管如此,有些场合该命令仍十分有用,比如说你想在一个特定地址放置你的代码。
.set symbol, expression
这个命令和.equ一样,选择哪个是个人的偏好,不过建议使用一致的命令。
.skip expression
在目标输出文件中跳过expression个字节。被跳过的字节的值是不可预计的,尽管它们一般被初始化为0。这个命令在声明有确定长度但没初始化值的变量特别有用。下面这个例子声明了三个变量,前两个已经初始化,最后一个(buffer)没有初始化。
head_ptr: .word 0 ; 初始化为0
tail_ptr: .word 0 ; 初始化为0
buffer: .skip 512 ; 512个字节未初始化
请参考 .ascii, .asciz, .byte, .hword and .word 等初始化值命令。
.text
选择该命令以下的内容位于最终可执行文件的text(代码)段。汇编程序指令必须放置在这个段。请参考.data命令的相对详细说明。
.word expression …
.4byte expression …
在对象文件中插入字(32位),内容为expression的值。可以同时插入多个,以","分隔。这个两个命令是一样的。下面的例子在对象文件中插入8个字节内容。
.word 0xDEADBEEF ; 插入字节: 0xEF 0xBE 0xAD 0xDE
.4byte -42 ; 插入字节 0xD6 0xFF 0xFF 0xFF
; 假设小端模式
注意ARM微控制器在32位地址边界上,一次总是读取32位数据,换句话说,它不能一次读取32位数据在一个最低两位不为0的地址上。请参考.align命令寻找解决办法。同时参考.byte和.hword命令。这些在其他处理器中不起眼的细节必须记住,在ARM术语中,字表示32位,而非16位。
Expressions(表达式)
许多ARM汇编指令甚至汇编命令需要不同类型的整数来做操作数,象"mov r0,#1",需要数字"1"。可以这么认为,一个表达式任何地方都可能会出现数字。
在最基本的层面上,一个表达式可以是一个简单的数字。这个数字可以表现为十进制(不带前缀),十六进制(0x或0X前缀),八进制(以0开头)或者二进制(0b或0B前缀)。它同样可以表现为字符常量,其形式为'a'或'a。下面例子中6行代码都向ro赋相同的值:74。
mov r0,#74 ; 十进制数 74
mov r0,#0x4A ; 十六进制数 0x4A (0X4A 和0x4a 是一样的)
mov r0,#0112 ; 八进制数 0112
mov r0,#0b1001010 ; 二进制数 0b1001010 (0B1001010 是一样的)
mov r0,#'J' ; 字符常量 "J" (首选语法)
mov r0,#'J ; 字符常量 "J" (备选语法)
当然,一个好的程序将用以下的方法来定义一个记号:
.set letter_J, 'J' ; 这是一个实际设计例子
mov r0, #letter_J
字符常量可以像C语言中那样包含无固定顺序的反斜杠,更多细节请参考GNU Assembler Reference的3.6节
除简单的整数外,表达式可以看起来像C语言中标准的数学和逻辑表达式一样,GNU Assembler Reference的第六章将详细描述这些方式。下面是一些例子:
.set ROM_size, 128 * 1024 ; 131072 字节 (128KB)
.set start_ROM, 0xE0000000
.set end_ROM, start_ROM + ROMsize ; 0xE0020000
.set bm1, 0b11001101 ; 二进制掩码 (十六进制: 0xCD)
.set val1, -2 * 4 + (45 / (5 << 2)) ; -6 (in two's complement)
.set val2, ROM_size >> 10 ; 128 (131072 右移 10位)
.set val3, bm1 | 0b11110000 ; bm1 OR 0b11110000 = 0b11111101
就像上面的例子一样,表达式同样可以包含已定义的标号。这样,表达式可以产生绝对的值和相对的的值,绝对值和它们在最终文件中的位置无关,是一个简单的数字常量。另一方面,相对值和一些地址(比如说.data的地址)有关,它们的最终位置取决于链接器生成最终可执行文件的时候,和汇编器无关。
相对值一般用于计算偏移,并且只能用"+"和"-"操作符。以下是例子:
.text
code_start:
XXX ; 汇编指令...
code_end:
.set instr_size, 4 ; 在ARM中指令都是4个字节长
.set code_length, code_end - code_start
.set first_instr, code_start
.set instr_11th, code_start + 10 * instr_size
.set instr_10th, instr_11th - instr_size
code_length标号被设置成一个绝对的值(code_end 和code_start的差值)。其他的标号,first_instr,instr_11th 和 instr_10th的值都取决于.text的值,它们真正的值只有在最终可执行文件生成时才能知道。
一般来说,以下规则适用于相对表达式:
相对值 + 绝对值 ->? 相对值
绝对值 + 相对值 ->? 相对值
相对值 – 绝对值 ->? 相对值
相对值 – 相对值 ->? 绝对值
所有其他包括相对值的表达式都是非法的(当然除了简单的标号自身的情况),请注意标号的相对计算只能基于在同一段的标号,比如说你不能得到一个位于.text段的标号和一个位于.data段标号之间的差值。
如果你需要,可以找到更多关于相对表达式的信息在GNU Assembler Reference的4.1节。
程序例子
正像开始时所说的,examples 文件夹 和它的子文件夹包含很多汇编语言例子程序。这些例子涉及基于ARM微控制器的GNU汇编器各个方面,建议你去学习这些例子,最起码要快速的浏览一遍!
特别是examples/intro目录中的那些例子文件,请按下面的顺序学习:
simple.s 一个简单的ARM汇编程序, 可以用它来起步
subr.s 简单的之程序 (函数调用)
values.s 给寄存器装载常量, 使用 ldr =
pseudo.s 更多的信息关于ARM伪指令
jumptbl.s 分支调用函数
wordcopy.s 拷贝一块存储在.data段中的多个字组成的区域
blockcopy.s 用堆栈来拷贝一块集中区域的数据
copy.s 拷贝一个以null结尾的字符串到内存 (汇编模块)
strcopy-a.s 拷贝字符串使用多个源文件, 需要用到copy.s
strcopy-c.c 拷贝字符串使用C和汇编混合编程, 需要用到copy.s
你可以通过拷贝那个文件夹下所有的文件到一个临时文件夹来建立联合的可执行文件,使用make:
mkdir –p ~/intro # 建立文件夹来放置文件
cd /mnt/cdrom/examples/intro # 假设 CD-ROM 被加载在 /mnt/cdrom
cp * ~/intro # 拷贝文件
cd ~/intro # 进入文件夹
chmod 644 * # 修改这些文件的权限为可读写
make all # 生成可执行文件
你可以用arm-elf-gdb或arm-elf-insight提供的仿真器来仿真运行这些可执行文件,更多信息请参考GNU Debugger的An Introduction部分。
更多信息
你可以在GNU Assembler Reference中被叫做Using AS的部分找到更多关于GNU汇编器的信息,这个200多页的文档相当的全面虽然界面不算友好。在 gnutools/doc 目录下可以找到这个文档。
对GNU汇编器来说最权威的参考就是真实的源代码。你可以在gnutools/src/binutils-version.tar.gz文件中找到它们。在解压完压缩文件并打上相应的补丁后,试着在gas子目录中浏览源代码文件。