Intel格式和ATT格式汇编区别
⼀、AT&T 格式Linux 汇编语法格式
1. 在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀;⽽在 Intel 汇编格式中,寄存器名不需要加前缀。例如:
AT&T格式Intel格式
pushl %eax push eax
2.在 AT&T 汇编格式中,⽤ '$' 前缀表⽰⼀个⽴即操作数;⽽在 Intel 汇编格式中,⽴即数的表⽰不⽤带任何前缀。例如:
AT&T格式Intel格式
pushl $1push 1
3.AT&T 和 Intel 格式中的源操作数和⽬标操作数的位置正好相反。在 Intel 汇编格式中,⽬标操作数在源操作数的左边;⽽在 AT&T 汇编格式中,⽬标操作数在源操作数的右边。例如:
AT&T格式Intel格式
addl $1, %eax add eax, 1
4.在AT&T 汇编格式中,操作数的字长由操作符的最后⼀个字母决定,后缀'b'、'w'、'l'分别表⽰操作数为字节(byte,8 ⽐特)、字(word,16 ⽐特)和长字(long,32⽐特);⽽在 Intel 汇编格式中,操作数的字长是⽤ "byte ptr" 和 "word ptr" 等前缀来表⽰的。例如:
AT&T格式Intel格式
movb val, %al mov al, byte ptr val
5.在 AT&T 汇编格式中,绝对转移和调⽤指令(jump/call)的操作数前要加上'*'作为前缀,⽽在 Intel 格式中则不需要。
6.远程转移指令和远程⼦调⽤指令的操作码,在AT&T 汇编格式中为"ljump" 和 "lcall",⽽在Intel 汇编格式中则为"jmp far" 和 "call far",即:
AT&T格式Intel格式
ljump $section, $offset jmp far section:offset
lcall $section, $offset call far section:offset
7.与之相应的远程返回指令则为:
AT&T格式Intel格式
lret $stack_adjust ret far stack_adjust
8.在 AT&T 汇编格式中,内存操作数的寻址⽅式是
section:disp(base, index, scale)
9.⽽在 Intel 汇编格式中,内存操作数的寻址⽅式为:
section:[base + index*scale + disp]
10.由于 Linux ⼯作在保护模式下,⽤的是 32 位线性地址,所以在计算地址时不⽤考虑段基址和偏移量,⽽是采⽤如下的地址计算⽅法:disp + base + index * scale
11.下⾯是⼀些内存操作数的例⼦:
AT&T格式Intel格式
movl -4(%ebp), %eax mov eax, [ebp - 4]
movl array(, %eax, 4), %eax mov eax, [eax*4 + array]
movw array(%ebx, %eax, 4), %cx mov cx, [ebx + 4*eax + array]
movb $4, %fs:(%eax)mov fs:eax, 4
⼆、Hello World!
既然所有程序设计语⾔的第⼀个例⼦都是在屏幕上打印⼀个字符串"Hello World!",那我们也以这种⽅式来开始介绍Linux 下的汇编语⾔程序设计。
在Linux 操作系统中,你有很多办法可以实现在屏幕上显⽰⼀个字符串,但最简洁的⽅式是使⽤Linux 内核提供的系统调⽤。使⽤这种⽅法最⼤的好处是可以直接和操作系统的内核进⾏通讯,不需要链接诸如libc 这样的函数库,也不需要使⽤ELF 解释器,因⽽代码尺⼨⼩且执⾏速度快。
Linux 是⼀个运⾏在保护模式下的 32 位操作系统,采⽤ flat memory 模式,⽬前最常⽤到的是 ELF 格式的⼆进制代码。⼀个 ELF 格式的可执⾏程序通常划分为如下⼏个部分:.text、.data 和 .bss,其中 .text 是只读的代码区,.data 是可读可写的数据区,⽽ .bss 则是可读可写且没有初始化的数据区。代码
区和数据区在ELF 中统称为section,根据实际需要你可以使⽤其它标准的section,也可以添加⾃定义section,但⼀个 ELF 可执⾏程序⾄少应该有⼀个 .text 部分。下⾯给出我们的第⼀个汇编程序,⽤的是 AT&T 汇编语⾔格式:
例1. AT&T 格式
#hello.s
.data # 数据段声明
msg : .string "Hello, world!\\n" # 要输出的字符串
len = . - msg # 字串长度
.text # 代码段声明
.text # 代码段声明
.global _start # 指定⼊⼝函数
_start: # 在屏幕上显⽰⼀个字符串
movl $len, %edx # 参数三:字符串长度
movl $msg, %ecx # 参数⼆:要显⽰的字符串
movl $1, %ebx # 参数⼀:⽂件描述符(stdout)
movl $4, %eax # 系统调⽤号(sys_write)
int $0x80 # 调⽤内核功能
# 退出程序
movl $0,%ebx # 参数⼀:退出代码
movl $1,%eax # 系统调⽤号(sys_exit)
int $0x80 # 调⽤内核功能
初次接触到AT&T 格式的汇编代码时,很多程序员都认为太晦涩难懂了,没有关系,在Linux 平台上你同样可以使⽤Intel 格式来编写汇编程序:
例2. Intel 格式
; hello.asm
section .data ; 数据段声明
msg db "Hello, world!", 0xA ; 要输出的字符串
len equ $ - msg ; 字串长度
section .text ; 代码段声明
global _start ; 指定⼊⼝函数
_start: ; 在屏幕上显⽰⼀个字符串
mov edx, len ; 参数三:字符串长度
mov ecx, msg ; 参数⼆:要显⽰的字符串
mov ebx, 1 ; 参数⼀:⽂件描述符(stdout)
mov eax, 4 ; 系统调⽤号(sys_write)
int 0x80 ; 调⽤内核功能
; 退出程序
mov ebx, 0 ; 参数⼀:退出代码
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 这⼀系统调⽤来实现的。根据上⾯的约定,参数fb、buf 和count 分别存在寄存器ebx、e c x 和edx 中,⽽系统调⽤号 SYS_write 则放在寄存器 eax 中,当 int 0x80 指令执⾏完毕后,返回值可以从寄存器 eax 中获得。
或许你已经发现,在进⾏系统调⽤时⾄多只有 5 个寄存器能够⽤来保存参数,难道所有系统调⽤的参数
个数都不超过 5 吗?当然不是,例如mmap 函数就有 6 个参数,这些参数最后都需要传递给系统调⽤ SYS_mmap:
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);c
当⼀个系统调⽤所需的参数个数⼤于 5 时,执⾏int 0x80 指令时仍需将系统调⽤功能号保存在寄存器 eax 中,所不同的只是全部参数应该依次放在⼀块连续的内存区域⾥,同时在寄存器 ebx 中保存指向该内存区域的指针。系统调⽤完成之后,返回值仍将保存在寄存器 eax 中。
由于只是需要⼀块连续的内存区域来保存系统调⽤的参数,因此完全可以像普通的函数调⽤⼀样使⽤栈(stack)来传递系统调⽤所需的参数。但要注意⼀点, Linux 采⽤的是 C 语⾔的调⽤模式,这就意味着所有参数必须以相反的顺序进栈,即最后⼀个参数先⼊栈,⽽第⼀个参数则最后⼊栈。如果采⽤栈来传递系统调⽤所需的参数,在执⾏ int 0x80 指令时还应该将栈指针的当前值复制到寄存器 ebx中。
五、命令⾏参数
在 Linux 操作系统中,当⼀个可执⾏程序通过命令⾏启动时,其所需的参数将被保存到栈中:⾸先是 argc,然后是指向各个命令⾏参数的指针数组argv,最后是指向环境变量的指针数据envp。在编写汇编语⾔程序时,很多时候需要对这些参数进⾏处理,下⾯的代码⽰范了如何在汇编代码中进⾏命令⾏参数的处理:
例3. 处理命令⾏参数
# args.s
.text
.globl _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 # 退出代码
ret
六、GCC 内联汇编
⽤汇编编写的程序虽然运⾏速度快,但开发速度⾮常慢,效率也很低。如果只是想对关键代码段进⾏优化,或许更好的办法是将汇编指令嵌⼊到 C 语⾔程序中,从⽽充分利⽤⾼级语⾔和汇编语⾔各⾃的特点。但⼀般来讲,在 C 代码中嵌⼊汇编语句要⽐"纯粹"的汇编语⾔代码复杂得多,因为需要解决如何分配寄存器,以及如何与C代码中的变量相结合等问题。
GCC 提供了很好的内联汇编⽀持,最基本的格式是:
__asm__("asm statements");
mmap格式怎么打开例如:
__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来负责。
在G C C 内联汇编语句的指令部中,加上前缀''%''的数字(如%0,%1)表⽰的就是需要使⽤寄存器的"样板"操作数。指令部中使⽤了⼏个样板操作数,就表明有⼏个变量需要与寄存器相结合,这样GCC和GAS在编译和汇编时会根据后⾯给定的约束条件进⾏恰当的处理。由于样板操作数也使⽤'' %''作为前缀,因此在涉及到具体的寄存器时,寄存器名前⾯应该加上两个''%'',以免产⽣混淆。
紧跟在指令部后⾯的是输出部,是规定输出变量如何与样板操作数进⾏结合的条件,每个条件称为⼀个"约束",必要时可以包含多个约束,相互之间⽤逗号分隔开就可以了。每个输出约束都以''=''号开始,然后紧跟⼀个对操作数类型进⾏说明的字后,最后是如何与变量相结合的约束。凡是与输出部中说明的操作数相结合的寄存器或操作数本⾝,在执⾏完嵌⼊的汇编代码后均不保留执⾏之前的内容,这是GCC在调度寄存器时所使⽤的依据。
输出部后⾯是输⼊部,输⼊约束的格式和输出约束相似,但不带''=''号。如果⼀个输⼊约束要求使⽤寄存器,则GCC在预处理时就会为之分配⼀个寄存器,并插⼊必要的指令将操作数装⼊该寄存器。与输⼊部中说明的操作数结合的寄存器或操作数本⾝,在执⾏完嵌⼊的汇编代码后也不保留执⾏之前的内容。
有时在进⾏某些操作时,除了要⽤到进⾏数据输⼊和输出的寄存器外,还要使⽤多个寄存器来保存中间
计算结果,这样就难免会破坏原有寄存器的内容。在GCC内联汇编格式中的最后⼀个部分中,可以对将产⽣副作⽤的寄存器进⾏说明,以便GCC能够采⽤相应的措施。
下⾯是⼀个内联汇编的简单例⼦:
例4.内联汇编
int main()
{
int a = 10, b = 0;
__asm__ __volatile__("movl %1, %%eax;\\n\\r"
"movl %%eax, %0;"
:"=r"(b)
:"r"(a)
printf("Result: %d, %d\\n", a, b);
}
上⾯的程序完成将变量a的值赋予变量b,有⼏点需要说明:
变量b是输出操作数,通过%0来引⽤,⽽变量a是输⼊操作数,通过%1来引⽤。
输⼊操作数和输出操作数都使⽤r进⾏约束,表⽰将变量a和变量b存储在寄存器中。输⼊约束和输出约束的不同点在于输出约束多⼀个约束修饰符''=''。
在内联汇编语句中使⽤寄存器eax时,寄存器名前应该加两个''%'',即%%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和edx
"S"和"D"寄存器esi、edi
"I"常数(0⾄31)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论