AT&T汇编

4,958 阅读6分钟

x86架构汇编指令一般有两种格式:

  • Intel汇编

    • DOS、Windows,包括我们之前了解的8086处理器
    • Windwos派系:VC编译器
  • AT&T汇编

    • Linux、Unix、Mac OS、iOS模拟器
    • Unix派系:GCC编译器

做为iOS开发工程师,接触到的汇编有两种:

  • AT&T汇编->iOS模拟器
  • ARM汇编->iOS真机

寄存器

16个常用寄存器

  • %rax, %rbx, %rcx, %rdx, %rsi, %rdi, %rbp, %rsp
  • %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15

寄存器的具体用途

  • %rax做为函数的返回值(同8086汇编的ax)
  • %rsp指向栈顶(同8086汇编的ss:sp)
  • %rdi、%rsi、%rdx、%rcx、%r8、%r9 等寄存器用于存放函数参数

如上图,rax寄存器还可以当eax寄存器来使用,很类似于于8086汇编中ax可以拆为ah和al一样,当寄存器中存入的值只需要32位就可以存储是,直接使用eax就可以,Xcode也是这样做的。

基本语法

这里主要基于8086汇编的区别来说明,所有要了解AT&T汇编,首先应该会汇编语言有基本的理解,如果完全不懂,需要先看从零入门8086汇编,了解基本的汇编基础。

寄存器表示

相比8086汇编,寄存器前面加了 % 。

; 8086
ax

; AT&T
%eax

单位

8086汇编

  • byte:字节,8bit
  • word:字,16bit

AT&T汇编

  • b:b=byte,8bit
  • s:s=short,16bit integer;32bit floating point
  • l:l=long,32bit integer;64bit floating point
  • q:q=quad,64bit
  • t:t=ten btyes,80bit floating point

; 8086 byte、word
mov byte ah, 4ch
mov word ax, 4ch

; AT&T 
movl %eax, %edx
movb $0x10, %al

语法

相比8086汇编,被操作的寄存器放在后面。

; 8086: 将ax寄存器的值存入dx
mov dx, ax

; AT&T: 将eax寄存器的 值存入edx
movl %eax, %edx

常数、立即数前面加 $ 。

; 8086: 将3赋值给ax 
mov ax, 3
; 8086: 将0x10赋值给ax
mov ax, 10H

; AT&T: 将3赋值给eax
movl $3, %eax
; AT&T: 将0x10赋值给eax
movl $0x10, %eax

lldb常用指令

读取寄存器的值

register read/格式

  • x 16进制
  • f 浮点
  • d 十进制
register read/x

修改寄存器的值

register write 寄存器名称 数值

register write $rax 0

读取内存中的值

x/数量-格式-字节大小 内存地址

字节大小:

  • b - byte 1字节
  • h - half word 2字节
  • w - word 4字节
  • g - giant word 8字节
x/3xw 0x0000010

修改内存中的值

memory write 内存地址 数值

memory wirte 0x0000010 10

expression 表达式

可以简写 expr

expression $rax
expression $rax = 1

po 表达式

po/x $rax
po (int)$rax

Xcode反汇编

创建一个Xcode的命令行项目,编写以下简单的代码:

#import <Foundation/Foundation.h>

int sum(int a, int b) {
    return a + b;
}

int main(int argc, const char * argv[]) {
    int c = sum(1, 2);
    
    printf("%d\n", c);
    return 0;
}

利用Xcode查看对应的汇编代码方法如下:

添加断点并运行:

Xcode选择Debug-Debug Workflow-Always Show Disassembly:

上如代码对应的AT&T汇编如下:

// 调用main函数
test001`main:
    // rbp寄存器入栈,用于恢复rbp,对应8086汇编中的push bp
    0x100000f40 <+0>:  pushq  %rbp
    // 将rsp赋值给rbp,对应8086汇编中的mov bp, sp
    0x100000f41 <+1>:  movq   %rsp, %rbp
    // 移动栈顶扩大栈容量用于存放临时变量,相当于8086汇编中的sub sp 0x20
    0x100000f44 <+4>:  subq   $0x20, %rsp
    
    0x100000f48 <+8>:  movl   $0x0, -0x4(%rbp)
    0x100000f4f <+15>: movl   %edi, -0x8(%rbp)
    0x100000f52 <+18>: movq   %rsi, -0x10(%rbp)
    
    // 利用edi、esi寄存器位函数传递参数
    // 将1存入edi寄存器
    0x100000f56 <+22>: movl   $0x1, %edi
    // 将2存入esi寄存器
    0x100000f5b <+27>: movl   $0x2, %esi
    // 调用sum函数
    0x100000f60 <+32>: callq  0x100000f20               ; sum 
    
// 调用sum函数
test001`sum:
    // rbp寄存器入栈,用于恢复rbp,对应8086汇编中的push bp
    0x100000f20 <+0>:  pushq  %rbp
    // 将rsp赋值给rbp,对应8086汇编中的mov bp, sp
    0x100000f21 <+1>:  movq   %rsp, %rbp
    
    // 将edi寄存器的值 1 存入栈
    0x100000f24 <+4>:  movl   %edi, -0x4(%rbp)
    // 将esi寄存器的值 2 存入栈
    0x100000f27 <+7>:  movl   %esi, -0x8(%rbp)
    
    // 从栈中将 1 存入 esi
->  0x100000f2a <+10>: movl   -0x4(%rbp), %esi
    // 从栈中将 2 加到 esi:esi = 1 + 2 = 3
    0x100000f2d <+13>: addl   -0x8(%rbp), %esi
    // 将 esi的值 3 存入 eax
    0x100000f30 <+16>: movl   %esi, %eax
    // 恢复 rbp 
    0x100000f32 <+18>: popq   %rbp
    // 返回函数
    0x100000f33 <+19>: retq   

上面可以看到,大体上和8086汇编调用函数的原理是一样的,不同的地方就是关于栈的操作有一些不同,但是基本流程我们是看得懂的。

当sum函数调用完成的时候,通过 register read/d 读取当前寄存器中的值:

可以发现rax=3,验证了AT&T也是使用rax寄存器来返回函数的结果的,类似于8086使用ax寄存器返回函数结果。

sum的汇编代码是通过在sum函数中插入断点运行得到的,不是在main函数的中插入断点得到的。

可以发现sum函数中,没有对rsp进行移动操作,这是因为编译器更加智能,它会检查函数中有没有调用其它函数,如果没有调用,就不会存在当前函数的栈空间因为没有移动rsp在调用另一个函数被覆盖的情况,所以就不需要sub 0x20 %rsp之类的操作。这种没有调用其它函数的函数叫做叶子函数。

Xcode编译器release模式的优化

使用Xcode编译项目都会知道,Xcode分为release和debug两种编译模式,而且都知道一点,就是release模式编译的程序会比debug的程序体积要小、运行速度更快,但是根本的原因在哪里呢?

下面找到Xcode-Build Settings-Apple Clang-Code Generation-Optimization Level,观察默认的选项:

测试代码:

#import <Foundation/Foundation.h>

int sum(int a, int b) {
    return a + b;
}

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    int c = sum(a, b);
    
    printf("%d\n", c);
    return 0;
}

在debug模式运行如下代码对应的反汇编指令:

和上面刚刚分析的反汇编基本一致的流程,包括rbp的保存与恢复、函数栈空间的管理、寄存器传递参数、调用sum方法等。

现在使用release模型运行项目,对应的反汇编指令:

第一感觉明显汇编指令少了很多,而且找不到寄存器传递参数、已经对sum函数的调用指令了,而且可以发现这句代码:movl &0x3, %esi,这里的3明显就是我们的加法计算的结果,没有通过函数计算就直接得到了。

编译器在release模式会对代码进行优化,将很多不需要函数调用就能够得到结果的运算全部直接转成计算后的汇编代码,所以运算更快,而且对应汇编指令的减少,程序体积同时也是减少。

Xcode中使用汇编混编

内联汇编

如下代码,在高级语言中插入汇编的方式实现计算 result = sum1 + sum2:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    int num1 = 1;
    int num2 = 6;
    int result;
    
    __asm__(
            // rax = num2 + num1
            "addq %%rbx, %%rax"
            // 将rax寄存器计算的结果赋值给result
            : "=a"(result)
            // 将num1给rbx, 将num2给rax
            : "a"(num1), "b"(num2)
            );
    
    printf("%d\n", result);
    
    return 0;
}

外联汇编

在头文件my_math.h中定义两个C语言函数,如下图:

在my_math.s中使用汇编实现这两个函数,如下图:

引用my_math.h使用定义的sum和minus函数:

#import <Foundation/Foundation.h>
#import "my_math.h"

int main(int argc, const char * argv[]) {
    int a = sum(1, 2);
    printf("%d\n", a);
    
    int b = minus(9, 3);
    printf("%d\n", b);
    return 0;
}