汇编格局 AT&T 与 Intel
《CSAPP》中为 AT&T 格局,《汇编语言 王爽》中为 Intel 格局
前言
机器指令是用二进制代码示意的 CPU 能够间接辨认和执行的一种指令系统的汇合 ,不同的 CPU 架构有不同的机器指令。汇编指令是机器指令便于记忆的书写格局,汇编指令编写实现后通过汇编器将其翻译成机器指令供 CPU 执行,因而, 汇编器的性能是将汇编指令翻译成机器指令
同一条机器指令能够用不同的汇编指令表白,确保汇编器执行时无谬误即可。不同的汇编指令格局衍生出不同的汇编语法且都有一个与之对应的汇编器。
随着计算机的倒退,不同厂家造成了自成一派的汇编语言,并有本人的汇编器。不同的汇编语言,实现雷同的机器指令的语法可能不统一
常见的汇编器有:
GAS (GNU Assembler)
,应用AT&T
语法格局MASM (Microsoft Macro Assembler)
,应用Intel
语法格局NASM (Netwide Assembler)
,应用的语法格局与Intel
相似,然而更简略FASM (Flat Assembler)
GAS 的 AT&T 的语法格局查问
MASMT 的 Intel 语法格局查问
语法格局
寄存器名
AT&T 中寄存器名要加前缀%
,而 Intel 则不须要。例如:
pushl %eax # AT&T 格局
push eax # Intel 格局
立刻操作数
AT&T 中用 $
前缀示意一个立刻数,而 Intel 不必带任何前缀。例如:
pushl $1 # AT&T
push 1 # Intel
操作方向
AT&T 与 Intel 中的源操作数和指标操作数的地位正好相同。
AT&T 指标操作数在源操作数的左边,Intel 指标操作数在源操作数的右边。例如:
addl $1,%eax # AT&T
add eax,1 # Intel
操作字长
AT&T 操作数的字长由操作数的最初一个字母决定,后缀以及示意字长如下:
b
:byte,8 比特(bit)w
:word,16 比特l
:long,32 比特
Intel 操作数的字长用 byte ptr
和word ptr
等指令前缀来示意。例如:
movb val,%al # AT&T
movl al,byte ptr val # Intel
相对转移和调用
AT&T 相对转移 jump
和调用 call
的操作数要退出 *
前缀,而 Intel 则不须要
近程转移和近程子调用
AT&T 近程转移指令 ljump
和近程子调用指令 lcall
,而 Intel 则为jmp far
和call far
ljump $section,$offset # AT&T
lcall $section,$offset
jmp far section:offset # Intel
call far section:offset
绝对应的近程返回
lret $stack_adjust # AT&T
ret far stack_adjust # Intel
内存操作数的寻址形式
AT&T 的格局为section:disp(base,index,scale)
,而 Intel 为section:[base + index*scale + disp]
上面是一些内存操作数的例子:
# AT&T
movl -4(%ebp),%eax
movl array(,%eax,4),%eax
movw array(%ebx,%eax,4),%cx
movb $4,%fs:(%eax)
# Intel
mov eax,[ebp - 4]
mov eax,[eax*4 + array]
mov cx,[ebx + 4*eax + array]
mov fs:eax,4
Hello World
在 Linux 操作系统中,最简洁的办法是应用内核提供的零碎调用。这种办法的最大益处时能够间接和操作系统内核进行通信,因为不须要链接诸如 libc
这样的函数库,也不须要应用 ELF
解释器,所以代码尺寸小且执行速度快。
Linux 是一个运行在保护模式下的 32 位操作系统,采纳 flat memory 模式,目前最罕用到的是 ELF
格局(Executable and Linkable Format,可执行可链接文件格式)的二进制代码。
一个 ELF 格局的可执行程序通常划分为如下几个局部:
.text
:只读的代码区.data
:可读写的数据区.bss
:可读写且没有初始化的数据区
代码区和数据区在 ELF 统称为 section
依据理论须要你能够应用其它规范的 section
,也能够增加自定义的section
,但一个 ELF 可执行程序至多应该有一个.text
局部。
上面给出一个输入 Hello, world! 的程序
# AT&T 格局;hello.s
.data # 数据段申明
msg : .string "Hello, world!\\n" # 要输入的字符串
len = . - msg # 字符串长度
.txt # 代码段申明
.global _start # 指定入口函数
_start: # 在屏幕上显示一个字符串
movl $len,%edx # 参数 3,字符串长度
movl $msg,%ecx # 参数 2,要显示的字符串
movl $1,%ebx # 参数 1,文件描述符(stdout)movl $4,%eax # 零碎调用号(sys_write)int $0x80 # 调用内核性能
# 退出程序
movl $0,%ebx # 参数 1,退出代码
movl $1,%eax # 零碎调用号(sys_exit)
int $0x80 # 调用内核性能
首次接触到 AT&T 格局的汇编代码时,很多程序员都认为太艰涩难懂了,没有关系,在 Linux 平台上同样能够应用 Intel 格局来编写汇编程序
; Intel 格局;hello.asm
section .data ; 数据段申明
msg db "Hello,world!",0xA ; 要输入的字符串
len equ $ - msg ; 字符串长度
section .text ; 代码段申明
global _start ; 指定入口函数
_start: ; 在屏幕上显示一个字符串
mov edx,len ; 参数 3,字符串长度
mov ecx,msg ; 参数 2,要显示的字符串
mov ebx,1 ; 参数 1,文件描述符(stdout)mov eax,4 ; 零碎调用号(sys_write)
int 0x80 ; 调用内核性能
; 退出程序
mov ebx,0 ; 参数 1,退出代码
mov eax,1 ; 零碎调用号(sys_exit)int 0x80 ; 调用内核性能
下面两个汇编程序采纳的语法尽管齐全不同,单功能都是调用 Linux 内核提供的 sys_write
来显示一个字符串,而后再调用 sys_exit
退出程序。在 Linux 内核文件 include/asm-i386/unistd.h
中,能够找到所有零碎调用的定义
零碎调用
即使是最简略的汇编程序,也不免用到输出、输入、退出等操作,要进行这些操作则须要调用操作系统提供的服务,也就是零碎调用。除非你的程序只实现加减乘除等数学运算,否则很难防止应用零碎调用,事实上各种操作系统的汇编程序除了零碎调用不同之外其它都是很相似的。
在 Linux 平台下有两种形式来应用零碎调用:
- 利用封装后的 C 库(libc)
- 通过汇编间接调用
其中通过汇编语言来应用零碎调用,是最高效的应用 Linux 内核服务的办法,因为最终生成的程序不须要与任何库进行链接,而是间接和内核通信
和 DOS 一样,Linux 下的零碎调用也是通过中断(int 0x80)来实现的。在执行 int 80
指令时,寄存器 eax 中寄存的是零碎调用的性能号,而传给零碎调用的参数则必须依照程序放到寄存器 ebx、ecx、edx、esi、edi 中,当零碎调用实现之后,返回值能够在寄存器 eax 中取得
所有的零碎调用性能号都能够在文件 /usr/include/bits/syscall.h
中找到,为了方便使用,它们是用 SYS_<name>
这样的宏来定义的,如 SYS_write
、SYS_exit
等。例如,常常用到的 write
函数是如下定义的:
ssize_t write(int fd, const void *buf, size_t count);
# 该函数的性能最终通过 SYS_write 这一系列调用来实现的
# 依据下面的约定,参数 fd、buf、count 别离存在寄存器 ebx、ecx、edx 中
# 而零碎调用号 SYS_write 则放在寄存器 eax 中,当 int 0x80 指令执行完后,返回值能够从寄存器 eax 中取得
在进行零碎调用时最多只有 5 个寄存器来保留参数 ,当一个零碎调用所需的 参数个数大于 5 时,执行 int 0x80 指令时仍需将零碎调用性能号保留在寄存器 eax 中,所不同的是全副参数应该顺次放在一块间断的内存区域(栈空间)里,同时在寄存器 ebx 中保留指向该内存区域的指针。零碎调用实现之后,返回值仍将保留在寄存器 eax 中
命令行参数
在 Linux 操作系统中,当一个可执行程序通过命令行启动时,其所需的参数将被保留到栈中:
- argc
- 指向各个命令行参数的指令数组 argv
- 指向环境变量的指令数据 envp
在编写汇编语言程序时,很多时候须要对这些参数进行解决,上面的代码师范了如何在汇编代码中进行命令行参数的解决:
# args.s
.txt
.global _start
_start:
popl %ecx # argc
vnext:
popl %ecx # argv
test %ecx,%ecx # 空指针表明完结
jz exit
movl %ecx,%ebx
xorl %edx,%edx
strlen:
movb (%ebx),%al
inc %edx
inc %ebx
test %al,%al
jnz strlen
movb $10,-1(%ebx)
movl $4,%eax # 零碎调用号(sys_write)
movl $1,%ebx # 文件描述符(stdout)int $0x80
jmp vnext
exit: movl $1,%eax # 零碎调用号(sys_exit)xorl %ebx,%ebx # 退出代码
int $0x80
ret
GCC 内联汇编
用汇编编写的程序尽管运行速度快,但开发速度十分慢,效率也低。如果只是绝对要害代码段进行优化,或者更好的方法是将汇编指令嵌入到 C 语言程序中,从而充分利用高级语言和汇编语言各自的特点。单一般来讲,在 C 代码中嵌入汇编语句要比“纯正”的汇编语言代码简单得多,因为须要解决如何调配寄存器,以及如何与 C 代码中的变量相结合等问题
GCC 提供了很好的内联汇编反对,最根本的格局是:__asm__("asm statements");
。例如:__asm__("nop");
如果须要同时执行多条汇编语句,则应该用 \\n\\t
将各个语句分隔,例如:
__asm__("pushl %%eax \\n\\t"
"movl $0,%%eax \\n\\t"
"popl %eax")
通常嵌入到 C 代码中的汇编语句很难做到与其它局部没有任何关系,因而更多时候用到残缺的内联汇编格局:__asm__("asm statements" : outputs : inputs : registers-modified)
插入到 C 代码中的汇编语句是以 :
分隔的四个局部 ,其中 第一个局部就是汇编代码自身,通常称为指令部,其格局在汇编语言中应用的格局基本相同。指令部是必须的,而其它局部则能够依据理论状况而省略。
在将汇编语句嵌入到 C 代码中时,操作数如何与 C 代码中的变量相结合是个很大的问题。GCC 采纳如下的办法来解决这个问题:程序员提供具体的指令,而对寄存器的应用则只须要给出 样板
和约束条件就能够了,具体如何将寄存器与变量联合起来齐全由 GCC 和 GAS 来负责的
在 GCC 内联汇编语句的指令部中,加上前缀 %
的数字(如:%0
、%1
)示意的就是须要应用寄存器的 样板
操作数。指令部中应用了几个样板操作数,就表明有几个变量须要与寄存器相结合,这样 GCC 和 GAS 在编译和汇编是会依据前面给定的约束条件进行失当的解决。因为样板操作数也应用了 %
作为前缀,因而在波及到具体的寄存器时,寄存器名后面应该是加上两个%
,免得混同
紧跟在 指令部前面的输入部 ,是规定输入变量如何与样板操作数进行联合的条件,每个条件称为一个“束缚”,必要时能够蕴含多个束缚,相互之间用逗号分隔开。每个输入的束缚都以=
号开始,而后紧跟一个对操作数类型阐明的字,最初是如何与变量相结合的束缚。但凡与输入部中阐明的操作数相结合的寄存器或操作数自身,在执行完嵌入的汇编代码后均不保留执行之前的内容,这是 GCC 在调度寄存器时所应用的根据
输入部前面是输出部 ,输出束缚的格局和输入束缚类似,但不带=
号。如果一个输出束缚要求应用寄存器,则 GCC 在预处理时就会为之调配一个寄存器,并插入必要的指令将操作数装入该寄存器。与输出部中阐明的操作数联合的寄存器或操作数自身,在指令完嵌入汇编代码后也不保留执行之前的内容
有时在进行某些操作时,除了要用到进行数据输出和输入的寄存器外,还要应用多个寄存器来保留两头计算的后果,这样就难免会毁坏原有寄存器的内容。在 GCC 内联汇编格局中最初一个局部中,能够对产生副作用的寄存器进行阐明,以便 GCC 可能采纳相应的措施
上面是一个内联汇编的简略例子:
#include <stdio.h>
int main()
{
int a = 10, b = 0;
__asm__ __volatile__("movl %1, %%eax;\n\r"
"movl %%eax,%0"
:"=r"(b)
:"r"(a)
:"%eax");
printf("result: %d, %d\n", a, b);
}
下面的程序是将变量 a 赋值到变量 b,有几点须要阐明:
b
是输入操作数,通过%0
来援用,而a
是输出操作数,通过%1
来援用- 输出操作数和输入操作数都应用
r
进行束缚,示意将两个变量存储在寄存器中。输出束缚和输入束缚的不同点在于输入束缚多一个束缚修饰符=
- 在内联汇编语句中应用寄存器 eax 时,寄存器名后面应该加两个
%
。内联汇编中应用%0
、%1
等来标识变量,任何纸袋一个%
的标识符都看成是操作数,而不是寄存器 - 内联汇编语句的最初一个局部高速 GCC 它将扭转寄存器 eax 中的值,GCC 在解决时不应应用该寄存器来存储任何其它的值
- 因为
b
被指定成输入操作数,当内联汇编语句执行结束后,它所保留的值将被更新
在内联汇编从用到的操作数从输入部的第一个束缚开始编号,从 0 开始,每个束缚计数一次,指令部要援用这些操作数是,只须要在序号前加上 %
作为前缀就能够了。须要留神的是,内联汇编语句的指令部在援用一个操作数总是将其作为 32 位的长字应用,但理论状况可能须要的是字或字节,因而在束缚中指明正确的限定符:
m
、v
、o
:内存单元r
:任何寄存器q
:寄存器 eax、ebx、ecx、edx 之一i
、h
:间接操作数E
、F
:浮点数g
:任意a
、b
、c
、d
:别离示意寄存器 eax、ebx、ecx、edxS
、D
:寄存器 esi、ediI
:常数(0~31)
See Also
- Intel 格局和 AT&T 格局汇编区别
- ELF 格局详解(一)
- 两类格调汇编语法比照