Plan9汇编语言备查
2013-02-21
本文源于对A Manual for the Plan 9 assembler的部分翻译
机器
这个汇编语言可以用于MIPS,SPARC,Intel 386,Intel 960,AMD 29000,Motorola 68020和68000, Motorola Power PC,AMD64,DEC Alpha,ARM.
寄存器
汇编语言中所有预定义的符号都是大号的.数据寄存器是R0到R7;地址寄存器是A0到A7; 浮点寄存器是F0到F7。
A6寄存器是供C编译器使用的,用于指向数据。A6寄存器是常量,必须在C程序初始化时 设置成外部定义的符号地址。
接着是硬件寄存器,比如在68020中的:CAAR,CACR,CCR,DFC,ISP,MSP,SFC,SR,USP,和VBR
汇编语言定义了一些伪寄存器,FP,SP和TOS用于栈操作。FP是桢寄存器,0(FP) 是第一个参数,4(FP)是第二个,依此类推。0(SP)是第一个自动变量。TOS是 top-of-stack寄存器,用于向procedure中推入参数,保存临时变量等等。(注:这里拿68020的硬件体系为例子的,看上去这个硬件体系有点像lisp语言的)
A7是硬件的栈地址寄存器,注意,混用A7和伪寄存器SP会出问题。汇编语言接受像p+0(FP)这种类似标签的指令,p是第一个参数。名称来自于符号表中,对最终的程序结果没有影响。
数据引用
所有的外部引用必须是相对某伪寄存器的,PC(program counter)或者SB(static base)。
BRA 2(PC)
允许使用标签,比如
BRA return NOP return: RTS
使用标签时没有PC标注
伪寄存器SB指程序的起始地址空间。引用全局数据写成对SB的偏移,比如
MOVL $array(SB), TOS
将一个全局数组的址址push到栈上,或者
MOVL array+4(SB), TOS
将数组的第二个元素进栈,注意偏移的使用。类似地,子例程调用必须使用SB:
BSR exit(SB)
文件静态变量使用符号
local <>+4(SB)
<>将会在加载时用一个独一无二的整数填充
当一个程序开始时,它必须在访问任何全局数据之前执行
MOVL $a6base(SB), A6
表达式
源文件会被C编译器预处理,因此#define和#include可以正常工作
寻址模式
o表示offset,d表示替换,是一个-128到127的常量.
放置数据
放到指令流:
LONG $12345
放到数据段用伪指令DATA,使用两个参数:放置的地址,包括大小,和放置的位置。 例如,定义一个字符串"abc":
DATA array+0(SB)/1, $' a' DATA array+1(SB)/1, $' b' DATA array+2(SB)/1, $' c' GLOBL array(SB), $4
或者
DATA array+0(SB)/4, $"abc\z" GLOBL array(SB), $4
/1定义字节数,GLOBL生成全局符号,$4说明符号占用多少字节。未初始化数据是自动 清0的。字符\z等价于C语言的\0.DATA中最多只能是8个字节
定义函数
入口点使用伪操作TEXT定义,接受函数名作为参数,以及自动预分配在栈上的字节数,
在写汇编程序时字节数一般是0。下面是一个返回两数之和的函数:
TEXT sum(SB), $0 MOVL arg1+0(FP), R0 ADDL arg2+4(FP), R0 RTS
还可以带个参数是控制优化的,1表示阻止优化,例如:
TEXT sum(SB), 1, $0 MOVL arg1+0(FP), R0 ADDL arg2+4(FP), R0 RTS
不会进行优化,而上面一个例子中会。带特殊状态的子例程,比如系统调用,不应该优化。
返回值放在R0中。浮点返回值放在F0中。返回结构体到C程序的函数,接受的第一个参数是 存储结果的地址,这种函数中调用协议不使用R0。调用函数要负责保存自己的参数(caller saves)。
指令集
NOP在loader中直接被消除,而不是一条什么都不做的指令。如果想生成什么都不做的指令, 使用WORD伪指令
i386
汇编器假定是32位保护模式。寄存器名是SP,AX,BX,CX,DX,BP,DI,和SI.栈指针是SP(不是伪寄存器)。返回值寄存器是AX。没有桢指针但是FP可以用为桢指针伪寄存器
二进制码名大多和Intel手册一样,L,W,B分别表示32位,16位,8位操作。除了loads,stores,conditionals例外。所有load和store来自通用寄存器,特殊寄存器(比如CR0,CR3,GDTR,IDTR,SS,CS,DS,ES,FS和GS)或者内存的操作写作:
MOVx src, dst
条件指令按68020而不是Intel汇编的习语,使用JOS,JOC,JCS,JCC,JEQ,JNE,JLS,JHI,JMI,JPL,JPS,JPC,JLT,JGE,JLE,JGT而不是JO,JNO,JB,JNB,JZ,JNZ,JBE,JNBE,JS,JNS,JP,JNP,JL,JNL,JLE,JNLE.
地址模式使用类似AX,(AX),(AX)(BX*4),10(AX),10(AX)(BX*4)的符号。相对AX的偏移可以换成FP或者SB来访问名称,例如extern+5(SB)(AX*2).
注意:非相对跳转JMP和CALL要加一个*符号。只有LOOP,LOOPEQ和LOOPNE是合法的循环指令。只有REP和REPN被当作重复。
AMD64
汇编器假定是64位模式。如果想改到32位模式,模式伪操作:
MODE $32
这个作用主要是检测给定的模式中指令是否合法,但是loader仍然假设是32位操作数和地址,调用和返回都是32位的PC。大多类似上面的386。体系结构中有额外的R8到R15。所有寄存器都是64位,但是指令会访问低8位,16位和32位。例如对AX进行MOVL会将低32位赋值,高32位清0。64位使用MOVQ。Plan 9的C语言使用额外寄存器是从R15往下。有一些MMX和XMM等指令。MMX寄存器是M0到M7,XMM寄存器是X0到X15。都统一使用L表示'long word'(32位),Q表示'quad word'(64位)。有些指令使用O('octword')表示128位。C语言的long long类型是64位的,但它是传递和返回的是值而不是引用。更要注意的是,C指针是64位的。AX仍然是返回值,但跟386不同的是,浮点返回值是X0。所有少于8字节的参数在栈中都是按8字节对齐的。
本来看这个的目的是源于对go语言汇编的学习,结果看了一圈发现意义不大,还不如直接看看各种情况下go生成的汇编码,在实践中学习。
func f(x,y int32) int32 { return x }
汇编出来之后是
--- prog list "f" --- 0000 (test.go:3) TEXT f+0(SB),$0-12 0001 (test.go:4) MOVL x+0(FP),BX 0002 (test.go:4) MOVL BX,.noname+8(FP) 0003 (test.go:4) RET ,
x+0(FP)中x是变量名x,这个好像没什么用,0(FP)这个是指第一个参数。.noname也是没什么用的。注意的是这里把最后的返回值放到了8(FP)。0(FP)是参数x,4(FP)是参数y,因此可以看出go语言的函数调用协议:返回值是挨着参数放在栈中的。这样就很容易解释多值返回了。
这个代码有点短,调用的时候被内联了。如果写长一点,再看看函数调用生成的汇编
f(3,4)
汇编之后
0034 (test.go:14) MOVL $3,(SP) 0035 (test.go:14) MOVL $4,4(SP) 0036 (test.go:14) CALL ,f+0(SB) 0037 (test.go:15) RET ,
这里可以看出参数的进栈顺序,SP之上依次是第一个参数,第二个参数…
有疑问加站长微信联系(非本文作者)