• 【翻译】在Linux平台上使用Intel和AT&T汇编语言以及GCC内联汇编


    本文翻译自 Using Assembly Language in Linux

    本文将描述 Linux 平台下的汇编语言编程。本文包含 Intel 和 AT&T 语法 asm 之间的比较、系统调用使用指南和 gcc 中内联 asm 使用入门指南。

    Intel汇编语法和AT&T汇编语法的区别

    Intel 和 AT&T 语法汇编语言在外观上有很大的不同,如果先学习 Intel 语法后第一次遇到 AT&T 语法,就会产生混淆,反之亦然。因此,让我们从基础开始。

    寄存器前缀或立即数前缀

    在 Intel 语法中,没有寄存器前缀或立即数前缀。然而,在 AT&T 中,寄存器的前缀为“%”,立即数的前缀为“$”。Intel 语法十六进制或二进制立即数数据的后缀分别为“h”和“b”。此外,如果第一个十六进制数字是字母,则该值的前缀为“0”。

    示例:

    Intel 语法 AT&T 语法 备注
    mov eax,1 movl \(1,%eax | AT&T 语法中,寄存器的前缀为“%”,立即数的前缀为“\)
    mov ebx,0ffh movl $0xff,%ebx
    int 80h int $0x80

    关于十六进制数表示方法的不同:

    汇编语法 示例 描述
    Intel 语法 80h
    0E9h
    Intel 语法中用后缀“h”来标识十六进制的数,如果该数以字母起首,在前面会增加一个“0”
    AT&T 语法 $0x80
    $0xff
    AT&T 语法中立即数以“$”作为前缀

    操作数的顺序

    Intel 语法中的操作数顺序与 AT&T 语法相反。在 Intel 语法中,第一个操作数是目标,第二个操作数是源,而在 AT&T 语法中,第一个操作数是源,第二个操作数是目标。

    示例:

    Intel 语法 AT&T 语法 备注
    instr dest,source instr source,dest Intel 语法目标操作数在前,AT&T 语法目标操作数在后
    mov eax,[ecx] movl (%ecx),%eax

    内存操作数

    在 Intel 语法中,基址寄存器包含在“[”和“]”中,而在 AT&T 语法中,基址寄存器包含在“(”和“)”中。

    示例:

    Intel 语法 AT&T 语法 备注
    mov eax,[ebx] movl (%ebx),%eax Intel 语法用“[”和“]”包裹,AT&T 语法则用“(”和“)”包裹
    mov eax,[ebx+3] movl 3(%ebx),%eax

    与 Intel 语法相比,涉及复杂操作的指令的 AT&T 格式非常模糊。这些函数的英特尔语法形式为 segreg:[base+index*scale+disp]。AT&T 语法形式为 %segreg:disp(base,index,scale)。

    index/scale/disp/segreg 都是可选的,可以简单地忽略。如果未指定比例 scale 并且指定了索引 index,则比例 scale 默认为 1。

    段寄存器 segreg 取决于指令以及应用程序是在实模式还是 pmode 下运行。在实模式下,它取决于指令,而在pmode中,它是不必要的。

    当用于缩放scale/显示disp时,使用的立即数不应在 AT&T 中加前缀“$”。

    示例:

    Intel 语法 AT&T 语法 备注
    instr foo,segreg:[base+index*scale+disp] instr %segreg:disp(base,index,scale),foo
    mov eax,[ebx+20h] movl 0x20(%ebx),%eax 在 AT&T 语法中,用于 disp 的立即数 应加前缀“$”,用于 base 的寄存器正常添加前缀“%”
    add eax,[ebx+ecx*2h] addl (%ebx,%ecx,0x2),%eax 在 AT&T 语法中,用于 scale 的立即数 应加前缀“$”
    lea eax,[ebx+ecx] leal (%ebx,%ecx),%eax 在 AT&T 语法中,如果未指定比例 scale 并且指定了索引 index,则比例 scale 默认为 1
    sub eax,[ebx+ecx*4h-20h] subl -0x20(%ebx,%ecx,0x4),%eax

    (指令)助记符后缀

    您可能已经注意到,AT&T语法(指令)助记符有一个后缀。这个后缀的意义是操作数大小的意义。“l”代表 long,“w”代表 word,“b”代表 byte。

    Intel 语法有类似的指令用于内存操作数,即 “dword ptr”、“word ptr”、“byte ptr”。

    示例:

    Intel 语法 AT&T 语法 备注
    mov al,bl movb %bl,%al 传送一个字节(8位)
    mov ax,bx movw %bx,%ax 传送一个字(两个字节,16位)
    mov eax,ebx movl %ebx,%eax 传送两个字(四个字节,32位)
    mov eax, dword ptr [ebx] movl (%ebx),%eax 传送两个字(四个字节,32位)
    mov rax,rbx movq %rbx,%rax 传送四个字(八个字节,64位)
    mov rax, qword ptr [rbx] movq (%rbx),%rax 传送四个字(八个字节,64位)

    系统调用

    本节将概述linux系统调用在汇编语言中的使用。
    syscalls由位于 /usr/man/man2 中的手册页面第二部分中的所有函数组成。它们也被列在 /usr/include/sys/syscall.h 中。下面是一个很好的列表 http://www.linuxassembly.org/syscall.html
    这些函数可以通过linux中断服务执行:int $0x80

    小于6个参数的系统调用

    对于所有系统调用,系统调用号在 %eax 中。对于少于六个参数的系统调用,参数按 %ebx、%ecx、%edx、%esi、%edi 的顺序排列。系统调用的 返回值 存储在 %eax 中。

    示例:
    根据 write(2) 手册页,write 被声明为 ssize_t write(int fd,const void* buf,size_t count);

    因此,fd 进入 %ebxbuf 进入 %ecxcount 进入 %edxsys_write 进入 %eax

    然后是执行系统调用的 int $0x80。系统调用的返回值存储在 %eax 中。

    $ cat write.s
    .include "defines.h"
    .data
    hello:
    	.string "hello world\n"
    
    .globl	main
    main:
    	movl	$SYS_write,%eax
    	movl	$STDOUT,%ebx
    	movl	$hello,%ecx
    	movl	$12,%edx
    	int	$0x80
    
    	ret
    $ 
    

    相同的过程适用于少于五个参数的系统调用。只需保持未使用的寄存器不变。具有可选额外参数的系统调用(如open或fcntl)将知道要使用什么。

    大于5个参数的系统调用

    参数个数大于5的系统调用仍然希望系统调用号(the syscall number)在 %eax 中,但参数在内存中排列,指向第一个参数的指针存储在 %ebx 中。

    如果你要使用堆栈(传递参数),参数从右(最后一个参数)向左(第一个参数)依次入栈。然后应将堆栈指针复制到 %ebx

    否则,将所有参数复制到分配的内存区域,并将第一个参数的地址存储在 %ebx 中。

    示例:
    在C语言中使用 mmap():

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <sys/mman.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    #define STDOUT	1
    
    void main(void) {
    	char file[]="mmap.s";
    	char *mappedptr;
    	int fd,filelen;
    
    	fd=fopen(file, O_RDONLY);
    	filelen=lseek(fd,0,SEEK_END);
    	mappedptr=mmap(NULL,filelen,PROT_READ,MAP_SHARED,fd,0);
    	write(STDOUT, mappedptr, filelen);
    	munmap(mappedptr, filelen);
    	close(fd);
    }
    

    mmap() 参数在内存中的排列:

    %esp %esp+4 %esp+8 %esp+12 %esp+16 %esp+20
    00000000 filelen 00000001 00000001 fd 00000000

    C代码等效于以下ASM 代码:

    $ cat mmap.s
    .include "defines.h"
    
    .data
    file:
    	.string "mmap.s"
    fd:
    	.long 	0
    filelen:
    	.long 	0
    mappedptr:
    	.long 	0
    
    .globl main
    main:
    	push	%ebp
    	movl	%esp,%ebp
    	subl	$24,%esp
    
    //	open($file, $O_RDONLY);
    
    	movl	$fd,%ebx	// save fd
    	movl	%eax,(%ebx)
    
    //	lseek($fd,0,$SEEK_END);
    
    	movl	$filelen,%ebx	// save file length
    	movl	%eax,(%ebx)
    
    	xorl	%edx,%edx
    
    //	mmap(NULL,$filelen,PROT_READ,MAP_SHARED,$fd,0);
    	movl	%edx,(%esp)
    	movl	%eax,4(%esp)	// file length still in %eax
    	movl	$PROT_READ,8(%esp)
    	movl	$MAP_SHARED,12(%esp)
    	movl	$fd,%ebx	// load file descriptor
    	movl	(%ebx),%eax
    	movl	%eax,16(%esp)
    	movl	%edx,20(%esp)
    	movl	$SYS_mmap,%eax
    	movl	%esp,%ebx
    	int	$0x80
    
    	movl	$mappedptr,%ebx	// save ptr
    	movl	%eax,(%ebx)
    		
    // 	write($stdout, $mappedptr, $filelen);
    //	munmap($mappedptr, $filelen);
    //	close($fd);
    	
    	movl	%ebp,%esp
    	popl	%ebp
    
    	ret
    $
    

    注意:上面的源代码清单与本文末尾的示例源代码不同。上面列出的代码没有显示其他系统调用,因为它们不是本节的重点。上面的源代码也只打开 mmap.s,而示例源读取命令行参数。mmap 示例还使用 lseek 获取文件大小。

    套接字系统调用仅使用一个系统调用号:SYS_socketcall,位于 %eax中。套接字函数通过位于 /usr/include/linux/net.h 中的子函数号进行标识,并且存储在 %ebx 中。指向系统调用参数的指针存储在 %ecx 中。套接字系统调用也使用 int $0x80执行。

    $ cat socket.s
    .include "defines.h"
    
    .globl	_start
    _start:
    	pushl	%ebp
    	movl	%esp,%ebp
    	sub	$12,%esp
    
    //	socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    	movl	$AF_INET,(%esp)
    	movl	$SOCK_STREAM,4(%esp)
    	movl	$IPPROTO_TCP,8(%esp)
    
    	movl	$SYS_socketcall,%eax
    	movl	$SYS_socketcall_socket,%ebx
    	movl	%esp,%ecx
    	int	$0x80
    
    	movl 	$SYS_exit,%eax
    	xorl 	%ebx,%ebx
    	int 	$0x80
    
    	movl	%ebp,%esp
    	popl	%ebp
    	ret
    $
    

    GCC 内联 ASM

    关于GCC内联asm的本节仅涵盖x86应用程序。操作数约束在其他处理器上会有所不同。清单的位置将在本文末尾。

    gcc中的基本内联组装非常简单。其基本形式如下:

    __asm__("movl	%esp,%eax");	// look familiar ?
    

    或者

    	__asm__("
    			movl	$1,%eax		// SYS_exit
    			xor	%ebx,%ebx
    			int	$0x80
    	");
    

    通过指定将用作asm输入、输出的数据以及将修改的寄存器,可以更有效地使用它。没有特定的输入/输出/修改字段是必需的。其格式为:

    __asm__("<asm routine>" : output : input : modify);
    

    输出和输入字段必须包含一个操作数约束字符串,后跟一个用括号括起来的C表达式。

    输出操作数约束前面必须有一个“=”,表示它是一个输出。

    可能有多个输出、输入和修改的寄存器。每个“条目”应以逗号(',')分隔,总共不应超过10个条目。

    操作数约束字符串可以包含完整寄存器名,也可以包含缩写。

    缩写表:

    缩写 寄存器/内存
    a %eax/%ax/%al
    b %ebx/%bx/%bl
    c %ecx/%cx/%cl
    d %edx/%dx/%dl
    S %esi/%si
    D %edi/%di
    m memory

    示例:

    __asm__("test	%%eax,%%eax", : /* no output */ : "a"(foo));
    

    或者

    __asm__("test	%%eax,%%eax", : /* no output */ : "eax"(foo));
    

    您还可以在 asm 之后使用关键字 volatile:通过在“asm”之后写入关键字“volatile”,可以防止“asm”指令被删除、显著移动或组合。
    (引自gcc信息文件中的“Assembler Instructions with C Expression Operands”部分。)

    $ cat inline1.c
    #include <stdio.h>
    
    int main(void) {
    	int foo=10,bar=15;
    	
    	__asm__ __volatile__ ("addl 	%%ebx,%%eax" 
    		: "=eax"(foo) 		// ouput
    		: "eax"(foo), "ebx"(bar)// input
    		: "eax"			// modify
    	);
    	printf("foo+bar=%d\n", foo);
    	return 0;
    }
    $
    

    %%eax

    您可能已经注意到,寄存器现在的前缀是“%%”,而不是“%”。这在使用输出/输入/修改字段时是必要的,因为也可以使用基于额外字段的寄存器别名。

    当命令中同时出现寄存器时,会以%%reg来引用寄存器(如上例中的%%eax),以便帮助gcc来区分寄存器和由C语言提供的操作数。

    寄存器别名(%0-%9)

    您可以简单地指定“a”,而不是写入“eax”并强制使用特定寄存器,例如“eax”或“ax”或“al”。其他通用寄存器也是如此(如Abbrev表所示)。在实际代码中使用特定寄存器时,这似乎没有用,因此gcc为您提供了寄存器别名。最大值为10(%0-%9),这也是仅允许10个输入/输出的原因。

    $ cat inline2.c
    int main(void) {
    	long eax;
    	short bx;
    	char cl;
    
    	__asm__("nop;nop;nop"); // to separate inline asm from the rest of
    				// the code
    	__volatile__ __asm__("
    		test	%0,%0
    		test	%1,%1
    		test	%2,%2"
    		: /* no outputs */
    		: "a"((long)eax), "b"((short)bx), "c"((char)cl)
    	);
    	__asm__("nop;nop;nop");
    	return 0; 
    }
    $ gcc -o inline2 inline2.c 
    $ gdb ./inline2
    GNU gdb 4.18
    Copyright 1998 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB.  Type "show warranty" for details.
    This GDB was configured as "i686-pc-linux-gnulibc1"...
    (no debugging symbols found)...
    (gdb) disassemble main
    Dump of assembler code for function main: 
    ... start: inline asm ... 
    0x8048427 : nop
    0x8048428 : nop 
    0x8048429 : nop 
    0x804842a : mov 0xfffffffc(%ebp),%eax 
    0x804842d : mov 0xfffffffa(%ebp),%bx
    0x8048431 : mov 0xfffffff9(%ebp),%cl 
    0x8048434 : test %eax,%eax 
    0x8048436 : test %bx,%bx
    0x8048439 : test %cl,%cl 
    0x804843b : nop 
    0x804843c : nop 
    0x804843d : nop 
    ... end: inline asm ... 
    End of assembler dump. 
    $ 
    

    如您所见,从内联asm生成的代码将(C语言)变量的值加载到(内联汇编)输入字段中分配给它们的寄存器中,然后继续执行实际代码。编译器根据变量的大小自动检测操作数大小,因此相应的寄存器由别名%0、%1和%2表示。(使用寄存器别名时在助记符中指定操作数大小可能会导致编译时出错)。

    别名也可以用于操作数约束。这不允许您在输入/输出字段中指定超过10个条目。我能想到的唯一用途是将操作数约束指定为“q”,这允许编译器在a、b、c、d寄存器之间进行选择。修改此寄存器时,我们将不知道选择了哪个寄存器,因此无法在修改字段中指定它。在这种情况下,您可以简单地指定“<number>”。

    示例:

    $ cat inline3.c
    #include <stdio.h>
    
    int main(void) {
    	long eax=1,ebx=2;
    
    	__asm__ __volatile__ ("add %0,%2"   
    		: "=b"((long)ebx)           // 输出
    		: "a"((long)eax), "q"(ebx)  // 输入:允许编译器在a、b、c、d之间进行选择
    		: "2"                       // 修改:因为我们不知道选择了那个寄存器,因此无法在修改字段中指定它,所以简单地指定了数字 2
    	);
    	printf("ebx=%x\n", ebx);
    	return 0;
    }
    $
    

    了解更多 GCC内联汇编怎么写?

  • 相关阅读:
    MogileFS的实现和bug解决
    MogileFS介绍
    SAMBA
    NFS
    测试DNS服务的命令
    DNS中的AC、rndc、智能DNS解析和基础排错
    DNS的主从、子域授权和转发服务器
    DNS域名记录
    DNS
    JavaScript设计模式与开发实践随笔(二)
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/16532497.html
Copyright © 2020-2023  润新知