开始之前

大名鼎鼎的Hello World

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.data 
msg : .string "Hello Word!\n"
len = . -msg

.text
.global _start

_start :
movl $len,%edx # len -> edx
movl $msg,%ecx # msg -> ecx
movl $1,%ebx
movl $4,%eax # 系统调用号
int $0x80 # 调用

movl $0,%ebx # 退出代码
movl $1,%eax
int $0x80

通用寄存器

汇编就是在几个寄存器间倒来倒去

寄存器有两种概念,逻辑上的和物理上的,分别是:

  • 架构相关寄存器(architectural register)
  • 物理寄存器(physical register)

前者是指令集(ISA)提供给编译器可见的,相当于API接口规范,一共16个通用寄存器;后者是硬件上实际设计的,软件领域不直接接触。最新的CPU可能有上百个实际的物理寄存器。对软件开发人员来说,我们只需要关注逻辑上的通用寄存器。

这16个逻辑上的通用寄存器如下所示:

寄存器 约束 惯例
rax 系统调用时,调用号
函数返回值
除法运算中,存放除数、以及运算结果的商
乘法运算中,存放被乘数、以及运算结果
rbx 被调用者保存 在32位模式下,用来存放GOT的地址
rcx 函数调用时,第4个参数
有时用作counter
rdx 函数调用时,第3个参数
除法运算中,存放运算结果的余数
乘法运算中,存放运算结果溢出的部分
rbp 被调用者保存 frame pointer,存放当前函数调用时栈的基地址
rsp 被调用者保存 时刻指向栈顶
rdi 函数调用时,第1个参数
rep movsb中的目的寄存器
rsi 函数调用时,第2个参数
rep movsb中的源寄存器
r8 函数调用时,第5个参数
r9 函数调用时,第6个参数
r10
r11
r12 被调用者保存
r13 被调用者保存
r14 被调用者保存
r15 被调用者保存

指令

1. 前缀、操作数的方向、操作码的后缀

关于前缀,AT&T 汇编中:

寄存器:“%”
立即数:“$”
十六进制数:“0x”

在AT&T 语境中说到386的通用寄存器时,会这样描述:8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp。比如:在stage1.s中,有这么一个定义:

#define ABS(x) (x-_start+0x7c00)

那么你就会知到0x7c00是个十六进制数(_start函数的入口地址就位于内存的0x7c00处)。而在设置int 0x13的0x42功能号时,它是这么说的:

movb $0x42,%ah

这句告诉了我们一些不同之处:

首先,操作码的后缀l表示的是操作码的大小:
l是长整数32位,
movw是16位,
movb是8位;

其次,立即数是用$前缀来表示的,就像$0x42;
再次,寄存器的名字是有%前缀的,像例子中的%ah;
最后,操作数的方向有点不一样,比如把立即数$0x42放 到寄存器%ah中,用的是movb $0x42,%ah,也即源操作数在前,目的操作数在后,这一点和intel汇编语法正好相反。

对于内存单元操作数来说,在AT&T 中是把寄存器用()括起来,而非[]。比如:

movl %ebx,8(%si)

将ebx寄存器里的值放到内存地址是8(%si)的内存单元上。正好,这里同时遇到了另一个问题,就是在AT&T 汇编中,间接寻址方式是有别于intel汇编的。上例中的8(%si)就相当于intel汇编中的[si+8]。

2,运算指令

一些常见的命令:

add %r10,%r11 # r10 + r11,结果放到r11 
add $5,%r10 # 5 + r10,结果放到r10
div %r10 # rax 除以r10,商放到rax,余数放到rdx
inc %r10 # r10 加1
mul %r10 # 将rax乘以 r10, 将结果放到rax中,溢出部分放到rdx 

3,拷贝指令

这些拷贝有从寄存器到寄存器、从寄存器到内存

mov %r10,%r11 #将r10寄存器的值赋值给r11 ;
mov $99,%r10 #将立即数99赋值给r10寄存器; 
mov %r10,(%r11) # 将r10的值拷贝到r11寄存器中的数值指向的内存地址上; 
mov (%r10),%r11 # 将r10中数值指向的内存地址上的内容拷贝到r11;push %r10 # 将r10的值放到栈上 ;
pop %r10 # 将栈顶的值pop到r10寄存器上。

4,流程控制指令

label(标号)程序控制语句

cmpb $GRUB_INVALID_DRIVE,%al
    je 1f
    movb %al,%dl
1:
    pushw %dx

上面就用到了标号,je 1f,前面的两个数进行比较,如果相等就跳转到1的位置。
注意,1后面的f表示的是forward,即从je指令后继续往前走来寻找1这个标号。所以,如果程序中有好几个叫做1的标号,就要看是1f还是1b了,b代表backward,方向和f相反。CivilNet BBS里有这么一个例子可以更好的帮助我们理解:

1: cmp  $0,  (%si)  
  je  1f        # 跳转到后面的1标示的地方,也就是第6行
  movsb  
  stosb  
  jmp  1b       # 跳转到前面1表示的地方  ,也就是第1行
1: jmp  1b      # 跳转到前面1表示的地方,第6行,其实就是个死循环

cmp %r10,%r11   # 比较r10 和 r11,根据比较结果来设置CPU的状态寄存器,从而影响后面的jump语句;
cmp $99,%r11    # 比较99和r11,根据比较结果来设置CPU的状态寄存器,从而影响后面的jump语句;
jmp label       #跳转到label je label #如果相等,跳转到label jne label       # 如果不相等,跳转到label 
jl label        # 如果小于,跳转到label 
jg label        # 如果大于,跳转到label 
call label      # 调用函数
ret             # 从函数调用返回
syscall         #系统调用 (32位模式下, 使用"int $0x80" 软中断)

AT&T 汇编指示符 Assembler Directive

所有的汇编指示符都由句号(’.’)开头。命令名的其余是字母,通常使用小写

1,.byte 表达式(expression_rs)

.byte可不带参数或者带多个表达式参数,表达式之间由逗点分隔。每个表达式参数都被汇编成下一个字节。在stage1.s中,有这么一段代码:

after_BPB:
CLI
.byte 0x80,0xca

那么编译器在编译时,就会在cli指令的下面接着放上0x80和0xca,因为每个表达式要占用1个字节,所以此处一共占用2个字节。

2,.word 表达式

这个表达式表示任意一节中的一个或多个表达式(同样用逗号分开),表达式占一个字(两个字节)。类似的还有.long。例:

.word 0x800

3,.file 字符(string)

.file 通告编译器我们准备开启一个新的逻辑文件。 string 是新文件名。

.file ”stage1.s”

4,.text 小节(subsection)

通知as编译器把后续语句汇编到编号为subsection的正文子段的末尾,subsection是一个纯粹的表达式。如果省略了参数subsection,则使用编号为0的子段。例:

.text

5,.code16

告诉编译器生成16位的指令

6,.globl

.globl使得连接程序(ld)能够看到symbol,如果gemfield在局部程序中定义了symbol,那么与这个局部程序链接的局部程序也能存取symbol,例:

.globl SYMBOL_NAME(idt) 

定义idt为全局符号。

7,.fill repeat , size , value

生成size个字节的repeat个副本。
repeat, size 和value都必须是纯粹的表达式。
repeat 可以是0或更大的值。
size 可以是0或更大的值, 但即使size大于8,也被视作8,以兼容其它的汇编器。

例如,在linux初始化的过程中,对全局描述符表GDT进行设置的最后一句为:

.fill NR_CPUS*4,8,0

编译和链接

linux下有两种方式,一种是使用汇编程序GAS和链接程序ld:

as filename.s –o filename.o
ld filename.o –o filename 

最终将源代码转换为目标文件.o再连接为可执行文件filename。另一种就是更上层的gcc(内部使用了as):

gcc –o filename filename.S