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的常量.

../img/asm0.png

放置数据

放到指令流:

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之上依次是第一个参数,第二个参数…

plan9汇编