ATT汇编(gas:gnu assembly)
前话
汇编不用多数,就是符号化了机器指令,是一种很低级(靠近硬件)的编程。很多人接触比较多的是 windows 下的 Intel 汇编,然而 unix/linux 下 or 使用 gcc 的话,用的是 ATT 汇编。(unix 最初是 AT&T 实验室中的 Ken Thompson 发明的。)
Intel 汇编和 ATT 汇编使用的指令基本一样,就是写法上有些差异,鉴于前者有很多人介绍,而后者相对比较少,故有了此篇文章。 (me 也曾经想在 linux 看下汇编的写法,然而搜到的基本都是 Intel 汇编,赶脚很失落,O__O"…)
ATT 汇编格式
ATT 汇编大体格式是: 指令 源操作数 目的操作数 ,比如将 10 移动到 eax 寄存器的写法: movl $10, %eax 。
- ATT 汇编的源操作数和目的操作数和 Intel 正好相反,也就是数据流向是从左到右;
- ATT 中立即数前需要加 $ 符号, 寄存器前加 % 符号;
- ATT 的指令加后缀 b 、 w、 l 、q 表明处理的数据长度,分别是字节、字(2B)、双字(4B)、四字(8B);
- ATT 以寄存器中的值为地址的内存单元的访问(间接寻址)是加上括号比如 (%eax),而非 Intel 的 [EAX] 。
ATT 常用指令
(1) 移动指令 movN src dst : movl $a, %ebx
(2) 运算指令 加 addN 、 减 subN 、按位与 andN 、按位异或 xorN src dst : xor %eax %eax (将 eax 值置为 0)
(3) 比较指令 cmpN src dst : cmp (%eax) 10
(4) 跳转指令 jmp、大于跳转 jg、小于跳转 jl 、大于等于跳转 jge、 jle、 je、 jne
(5) 函数调用 call 和返回 ret
ATT 汇编代码示例
(1) hello,world
上来先见识一下 hello,world 的写法:
-
.data
-
msg :
-
.ascii "hello,world\0"
-
-
.text
-
.globl _main
-
_main :
-
# 函数调用的“开场白”
-
pushl % ebp
-
movl % esp , % ebp
-
andl $ - 16 , % esp
-
-
# 调用 puts 函数
-
pushl $msg
-
call _puts
-
-
# 设置返回值
-
movl $ 0 , % eax
-
-
# 函数调用的“结束语”
-
leave
-
ret
-
上面 "hello,world" 字符串的输出是通过调用 c 的库函数 puts 实现的。 汇编上面的代码可以:$ gcc -o hello hello.s 。
(1) 在 linux 下汇编此程序如果说找不到 _start 那么将程序中的 _main 改成 _start 就好。
(2) 除了使用 gcc 命令之外,也可以先汇编 $ as -o hello.o hello.s 然后再链接 $ ld hello.o -o hello 生成可执行文件。(如果 windows 下说找不到 _puts 标识符,可以 $ld hello.o -lmsvcrt )。
程序解释:
(1) 实现 "hello,world" 打印输出的其实只有下面两行:
-
pushl $msg
-
call _puts
_main 是一个函数。汇编函数的写法有一些约定俗成的习惯,比如进入函数的“开场白” (第三行可以省略):
-
pushl % ebp
-
movl % esp , % ebp
-
andl $ - 16 , % esp
和退出函数的“结束语”:
-
leave
-
ret
(3) 数据段 .data 是定义函数外的数据的, 文本段 .text 就是通常所的代码段。
Article type:
ATT汇编代码实例
前面是汇编的 hello,world 程序。为什么借助于 c 的 puts 函数呢?那是因为只有借助于 c 标准库的可移植性达到汇编代码的——可移植性,因为 linux 和 windows 的更低一级的系统调用是不一样的。
程序实例 2 : 调用 c 的 printf 函数
程序解释:
(1) 函数的开场白(主要是两行,保存原来的 %ebp 和重新设定 %ebp) 就不多说了,结束语有两行:leave 和 ret 。 leave 实际上是开场白的“反操作”,具体的可以参看代码中的注释部分。
(2) c 的 printf 函数,在汇编中调用是 _printf 函数,如同例子 1 中的 _puts 而不是 puts 函数。
(3) 汇编中调用函数之前,需要将参数压入栈中,参数压入的顺序是从右到左。比如 printf("\d", 42); 调用先压入 42 再压入 "%d" 。
(4) 汇编中变量实际上都是标号 LABEL,$LABEL 取的是变量对应的地址,而 LABEL 则代表变量的值;$42 是常量 42 的写法;上面的汇编代码的两次 printf 调用输出结果是一样的。
c 中函数调用的一些细节
(1) 虽然标准 c 中没有说明函数调用时候参数求值的顺序(从左到右?),然而至少从 gas 中可以看到,参数是从右到左传递过去的;
(2) c 的被调用函数 callee 不负责存储参数,参数是调用者 caller 压入栈中的,参数的清理也是调用者 caller 的责任;
(3) 在汇编函数中, 8(%ebp) 是函数的从左到右数的第一个参数,12(%ebp) 是第二个参数。试问 4(%ebp) 是谁呢? 是 caller 的 %ebp 的值!也就是 %ebp 的旧值,caller 的 %ebp 的值!
精诚所至,金石为开