AT&T 汇编

1. 基础

  • x86 的寄存器为32位,x64 的寄存器为64位。寄存器间对应关系:

    64位寄存器	低32位	低16位	低8位
    rax			eax		ax		al
    rbx			ebx		bx		bl
    rcx			ecx		cx		cl
    rdx			edx		dx		dl
    rsi			esi		si		sil
    rdi			edi		di		dil
    rbp			ebp		bp		bpl
    rsp			esp		sp		spl
    r8			r8d		r8w		r8b
    r9			r9d		r9w		r9b
    r10			r10d	r10w	r10b
    r11			r11d	r11w	r11b
    r12			r12d	r12w	r12b
    r13			r13d	r13w	r13b
    r14			r14d	r14w	r14b
    r15			r15d	r15w	r15b
    
  • 在引用寄存器时,需要加上 % 前缀,如,%rax

  • 立即数以 $ 作为前缀,如,$0x1

  • 源操作数在前,目的操作数在后。

  • 指令后缀

    后缀		大小(字节)
    b		1
    w		2
    l		4
    q		8
    

    如,

    movl $0x4, %eax	# %eax = $0x4
    

2. 寻址

  • 通用寻址格式:偏移量(%基址寄存器, %索引寄存器, 比例因子);比例因子为1、2、4、8。
  • 有些项是可以被省略的,比例因子默认为1。
  • 最终地址为:偏移量 + %基址寄存器 + %索引寄存器 x 比例因子。
0x8(%edx)
(%edx, %ecx)
(%edx, %ecx, 4)
0x80(, %ecx, 4)

3. 过程调用

x86-32

寄存器使用惯例

  • 调用者负责保存和恢复的寄存器:%eax, %edx, %ecx
  • 被调用者负责保存和恢复的寄存器:%ebx, %esi, %edi
  • %eax 保存返回值。

栈帧

  • %esp 指向栈顶(低地址),%ebp 指向栈帧(高地址),%ebp ~ %esp 之间的区域就是栈帧。
  • 栈帧保存的内容(从 %ebp%esp):
    • 【此处是父过程的栈帧,与当前栈帧相关的内容包括:当前过程的输入参数(从右往左压入栈)、返回地址】 ;
    • 父过程的栈帧起始地址(旧的 %ebp);
    • 被保存的寄存器值;
    • 当前过程所用到的局部变量;
# set up
pushl %ebp		# 旧的 %ebp
movl %esp, %ebp	# 新的 %ebp
pushl %ebx		# 保存用到的寄存器

movl 12(%ebp), %ecx	# 第二个参数
movl 8(%ebp), %ecx	# 第一个参数

 ...

# finish
movl -4(%ebp), %ebx	# 恢复用到的寄存器
movl %ebp, %esp
popl %ebp
ret

x86-64

寄存器使用惯例

  • 被调用者负责保存和恢复的寄存器:%rbx, %rbp, %r10, %r12, %r13, %r14, %r15
  • 前六个参数依次位于 %rdi, %rsi, %rdx, %rcx, %r8, %r9 寄存器中,这些寄存器由调用者负责保存和恢复;如果参数大于六个,则余下参数还是从右往左压入栈;
  • %rax 保存返回值。

栈帧

完全基于 %rsp 完成。

# 保存用到的寄存器
movq %rbx, -16(%rsp)
movq %r12, -8(%rsp)

# 分配栈帧
subq $16, %rsp

 ...

# 恢复用到的寄存器
movq (%rsp), %rbx
movq 8(%rsp), %r12

# 释放栈帧
addq $16, %rsp

4. 系统调用

  • 通过执行中断 int $0x80 来实现。
  • 系统调用号保存在 %eax 中。
  • 系统调用参数按序放到 %ebx, %ecx, %edx, %esi, %edi 中。
  • 如果参数个数大于 5 个,则所有参数放到一个连续的内存区域中,然后将指向该区域的指针放到 %ebx 中。
  • 系统调用返回值放在 %eax 中。

5. 静态链接

  • 每个源文件都有自己的代码段和数据段,在程序链接期间,链接器会将多个文件的代码段和数据段集成为单一的代码段和数据段,如此之后,程序便有了一个统一的内存布局。
  • 接着将 .o 文件中的符号解析为地址,并将所有的符号引用更新为地址。
  • 静态链接库:由多个源文件生成多个 .o 对象文件,然后将这些对象文件打包归档成一个 .a 文件;在静态链接期间,如果 .a 文件中的某个成员能够匹配一个外部符号,则将该成员链接入可执行文件中。

6. Hello, World!

.data
msg:
	.string "Hello, World!\n"
	len = .-msg
	
.text
.global _start
_start:
	movl	$4, %eax	# 系统调用号,write 为 4
	movl	$1, %ebx	# fd = 1,表示 stdout
	movl	$msg, %ecx	# buf = $msg
	movl	$len, %edx	# count = $len
	int	$0x80

	movl	$1, %eax
	movl	$0, %ebx
	int	$0x80

64 位汇编:

$ as -o helloworld.o helloworld.s
$ ld -o helloworld helloworld.o
$ ./helloworld 
Hello, World!

64 位环境下生成 32 位汇编:

$ as --32 -o helloworld.o helloworld.s
$ ld -m elf_i386 -o helloworld helloworld.o
$ ./helloworld 
Hello, World!

7. 命令行参数

输出命令行参数:

.text
.global _start
_start:
	popl	%ecx	# argc
next_arg:
	popl	%ecx	# argv[i]
	test	%ecx, %ecx	# argv[i] == NULL
	jz	exit
	movl	%ecx, %ebx
	xorl	%edx, %edx	# strlen(argv[i])
strlen:
	movb	(%ebx), %al
	inc	%edx
	inc	%ebx
	test	%al, %al
	jnz	strlen
	movb	$10, -1(%ebx)	# ascii('\n') == 10

	movl	$4, %eax
	movl	$1, %ebx
	int	$0x80
	jmp	next_arg

exit:
	movl	$1, %eax
	xorl	%ebx, %ebx
	int	$0x80
$ ./arg hello world
./arg
hello
world

8. C程序 -> 汇编代码

64 位汇编:

$ gcc -S -O2 hello.c

64 位环境下生成 32 位汇编:

$ sudo apt install g++-multilib
$ gcc -S -O2 -m32 hello.c

9. 反汇编

反汇编对象文件:

$ gcc -c main.c -o main.o -m32 -g

# -D: Display assembler contents of all sections
# -S: Intermix source code with disassembly
# -r: Display the relocation entries in the file
$ objdump -DSr main.o

反汇编可执行文件:

$ gcc -o main main.c -g
$ objdump -DS main

10. 汇编指示

  • .text:代码段。

  • .data:带有初始值的数据段;声明一个数据元素时,需要指明数据类型及初始值;因为它具有初始值,所以它在可执行文件中实实在在具有指定大小的空间来保存这些值。

  • .bss:不带有初始值的数据段;声明一个数据元素时,不需要指明数据类型及初始值,只需声明保留一段内存即可;因为它不具有初始值,所以它在可执行文件中并没有分配指定大小的空间,而是在程序运行时才分配声明的内存。

  • .rodata:只读数据段。

  • .section:把代码划分为若干个区,当程序被加载进内存时,不同的区会被加载到不同的地方,且不同的区具有不同的读、写、执行权限;例如,.section .data.section .text

  • .p2align 4:按 2 的 4 次方,即 16 字节对齐。

  • .align 4:按 4 字节对齐。

  • .string "hello, world!":定义一个字符串,内容为 “hello, world!”。

  • .long 10:定义一个 long 类型的数据,值为 10。

  • .comm buffer, 1000:声明一个大小为 1000 字节的内存区域,并把区域的起始地址付给 buffer;该内存区域是未初始化的、全局可见的。

  • .lcomm buffer, 1000:声明一个大小为 1000 字节的内存区域,并把区域的起始地址付给 buffer;该内存区域是未初始化的、只局部可见的。

  • 符号:表示数据(变量)或指令(函数)的地址,后跟 :

  • .global main:表示 main 符号是全局可见的(同一程序的其他模块可访问),如果没有 .global 修饰,则该符号不是全局可见的。

  • .:表示当前地址。

  • .equ SYS_OPEN, 5:定义一个符号常量 SYS_OPEN,其值为 5。

  • .include "record.s":包含 record.s 文件至当前文件。

  • .rept n xxx .endr:重复 n 次 xxx。

    # 重复 31 次 .byte 0 
    # 用作数据填充
    .rept 31
    .byte 0
    .endr
    

11. gcc 内联汇编

基本格式:

asm [volatile] (
	assembler template 
  	: [output operands]
  	: [input operands]
  	: [list of clobbered registers]
)
  • volatile 告诉 gcc 不要优化掉这些汇编代码。

  • 每条汇编指令需要放在 "assembler template" 中,且需要以 \n 结尾。

  • 在汇编代码中,如果使用到寄存器,则需要使用两个 %,如,%%eax

  • 操作数约束:在输出、输入操作数中,可以通过操作数约束来限制操作数应该放在哪个寄存器或直接使用内存:"约束"(变量)

    +---+--------------------+
    | r |    Register(s)     |
    +---+--------------------+
    | a |   %eax, %ax, %al   |
    | b |   %ebx, %bx, %bl   |
    | c |   %ecx, %cx, %cl   |
    | d |   %edx, %dx, %dl   |
    | S |   %esi, %si        |
    | D |   %edi, %di        |
    | r |   任何一个通用寄存器  |
    | m |   使用内存          |
    +---+--------------------+
    
  • 修饰符 =, +, & 只能用于输出部分;= 表示当前输出表达式的属性为只写,+ 表示当前输出表达式的属性为可读可写,& 告诉 gcc 不得为任何输入操作表达式分配与此输出操作表达式相同的寄存器。

  • 数字占位符:在汇编代码中,可以使用 %0, %1, ... 来依次引用输出、输入中的寄存器。

    #include <stdio.h>
    
    int main() {
            int a = 1;
            int b = 2;
            int c;
    
            asm volatile (
                    "addl %1, %2\n"
                    "movl %2, %0\n"
                    : "=a"(c)				// 变量 c 放在寄存器 eax(%0) 中
                    : "b"(a), "c"(b)		// 变量 a, b 分别放在 eax(%1) 和 ebx(%2) 中
                    :
            );
    
            printf("c = %d\n", c);
    
            return 0;
    }
    
  • 名称占位符:此时表达式的格式为 [name] "约束"(变量),在汇编代码中可以使用 %[name] 来引用该操作数。

    int main() {
            int a = 1;
            int b = 2;
            int c;
    
            asm volatile (
                    "addl %[a], %[b]\n"
                    "movl %[b], %[c]\n"
                    : [c] "=a"(c)
                    : [a] "b"(a), [b] "c"(b)
                    :
            );
    
            printf("c = %d\n", c);
    
            return 0;
    }
    
  • clobbered registers 列表告诉 gcc 我们会使用和修改这些寄存器(不包括输入、输出寄存器),如,"eax, ecx";另外,如果汇编代码修改了内存,则还需要加上 memory 约束。

    #include <stdio.h>
    
    void my_memset(void* buf, char c, size_t count) {
            asm volatile (
                    "cld\n"
                    "rep stosb\n"
                    :
                    : "a"(c), "D"(buf), "c"(count)
                    : "memory"
            );
    }
    
    int main() {
            char buf[8];
            my_memset(buf, 'a', sizeof(buf));
    
            for (int i = 0; i < sizeof(buf); i++) {
                    printf("%c", buf[i]);
            }
            printf("\n");
    
            return 0;
    }
    
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值