个人汇编tip笔记

几个概念

立即数

1)把数据转换成二进制形式,从低到高写成 4位1组的形式,最高位一组不够4位的前面补0

2)数1的个数,如果大于8个【可能也是立即数,取反】不是立即数,如果小于等于8个 进行下面步骤

3)如果数据中间有连续的大于等于24个0,循环左移2的倍数,使高位全为0

4)找到最高位的1,去掉前面的最大偶数个0

5)找到最低位的1,去掉后面偶数个0

6)数剩下的位数,如果小于等于8位,那么这个数就是个立即数,反之就不是立即数

条件码

条件码:本条指令的执行,依赖于上一个指令的执行结果

img

寄存器

64bit系统中,对应的64bit寄存器名为RAX、RBX……

EIP存储着下一条指令的地址,每执行一条指令,该寄存器变化一次。

寄存器结构

ARM64架构

通用寄存器

图片

ARM64提供了31个通用寄存器,其用途如下表:

在这里插入图片描述

x0~x7:传递子程序的参数和返回值,使用时不需要保存,多余的参数用堆栈传递,64位的返回结果保存在x0中。
x8:用于保存子程序的返回地址,使用时不需要保存。
x9~x15:临时寄存器,也叫可变寄存器,子程序使用时不需要保存。
x16~x17:子程序内部调用寄存器(IPx),使用时不需要保存,尽量不要使用。
x18:平台寄存器,它的使用与平台相关,尽量不要使用。
x19~x28:临时寄存器,子程序使用时必须保存。
x29:帧指针寄存器(FP),用于连接栈帧,使用时必须保存。
x30:链接寄存器(LR),用于保存子程序的返回地址。
x31:堆栈指针寄存器(SP),用于指向每个函数的栈顶。

ARM64体系架构

图片

作者:Loyen 链接:https://www.jianshu.com/p/91c5dc0a8bb9

img

上图描述的是main函数调用func1函数的栈帧情况,从图可知,当main函数调用func1函数时,func1函数会先将PC、LR、SP、FP四个寄存器压到栈上边,其中SP和FP的值分别指向main函数栈帧的两个边界,LR的值保存的是func1调用结束之后的返回值,PC值表示的是当前执行到的指令地址,放置的是进入func1后的指令地址。紧接着就会在栈上分配一片区域,用于放置局部变量等。

/opt/aarch64_eabi_gcc6.2.0_glibc2.24.0_fp/bin/aarch64-unknown-linux-gnueabi-objdump -DSl UFPFCM > hu.txt


8086和AT&T

汇编语法主要有AT&T和8086汇编,两者语法规则存在异同。

架构指令
INTELMOV EAX,EBX
AT&Tmov %ebx,%eax

大小写1

INTEL格式的指令使用大写字母,而AT&T格式的使用小写字母

操作数赋值方向

在INTEL语法中,第一个表示目的操作数,第二个表示源操作数,赋值方向从右向左。

AT&T语法第一个为源操作数,第二个为目的操作数,方向从左到右。

前缀

在 INTEL 语法中寄存器和立即数不需要前缀;AT&T 中寄存器需要加前缀“%” ;立即数需要加前缀“$” 。

架构指令
INTELMOV EAX,1
AT&Tmovl $1,%eax
架构指令
INTELCALL FAR SECTION:OFFSETJMP FAR SECTION:OFFSETRET FAR SATCK_ADJUST
AT&Tlcall $secion:$offsetljmp $secion:$offsetlret $stack_adjust

间接寻址

INTEL 中基地址使用“[” 、“]” ,而在 AT&T 中使用“(”、“)” ;

另外处理复杂操作数的语法也不同,INTEL 为 Segreg:[base+index*scale+disp] ,而在 AT&T 中为%segreg:disp(base,index,sale),其中segreg,index,scale,disp都是可选的,在指定index而没有显式指定Scale的情况下使用默认值 1。Scale和 disp不需要加前缀“&” 。

架构指令
INTELfoo,segreg:[base+index*scale+disp]
AT&T%segreg:disp(base,index,scale)

后缀

AT&T 语法中大部分指令操作码的最后一个字母表示操作数大小

“b”表示 byte(一个字节) ;“w”表示 word(2 个字节) ;“l”表示 long(4 个字节) 。

INTEL 中处理内存操作数时也有类似的语法如:BYTE PTR、WORD PTR、DWORD PTR。

INTELAT&T
MOV AL,BLmovb %bl,%al
MOV AX,BXmovw %bx,%ax
MOV EAX,DWORD PTR [EBX]movl (%ebx),%eax

在 AT&T 汇编指令中,操作数扩展指令有两个后缀,一个指定源操作数的字长,另一个指定目标操作数的字长。AT&T 的符号扩展指令的为“movs” ,零扩展指令为“movz” (相应的 Intel指令为“movsx”和“movzx”) 。因此, “movsbl %al,%edx”表示对寄存器 al 中的字节数据进行字节到长字的符号扩展,计算结果存放在寄存器edx中。下面是一些允许的操作数扩展后缀:
bl: 字节->长字
bw: 字节->字
wl: 字->长字

跳转指令标号后的后缀表示跳转方向, “f” 表示向前 (forward) , “b” 表示向后 (back) 。


语法与指令2

立即数与符号常数

符号常数直接引用,不需要加前缀,如:movl value , %ebx,value为一常数;

在符号前加前缀$表示引用符号地址, 如movl $value, %ebx,是将value的地址放到 ebx中。

总线锁定

前缀“lock” :总线锁定操作。 “lock”前缀在Linux 核心代码中使用很多,特别是SMP代码中。当总线锁定后其它CPU不能存取锁定地址处的内存单元。远程跳转指令和子过程调用指令的操作码使用前缀“l“,分别为 ljmplcall,与之相应的返回指令伪lret

跳转和函数调用

AT&T语法中的跳转和函数调用指令(jmp, jcc, call等)比较特殊,如下表所示:

在这里插入图片描述

上表给出了如果跳转和函数调用的操作数分别是寄存器、立即数、内存,该如何表示地址:

寄存器:如果想要跳到寄存器%rax内容对应的地址,不是写成jmpq %rax,而是写成jmpq *%rax
立即数:如上表所示,不用加*号
内存:如果想要跳转到%rip + 0x10对应的地址,不是写成jmpq 0x10(%rip),而是写成jmpq *0x10(%rip)


C内联汇编

基本内联汇编(Basic Inline)

基本内联汇编的格式比较简单。如下:

asm("assembly code");

如果内联汇编有多条指令,则每行都要加上双引号,并且该行要以 \n\t 结尾。这是因为 GCC 会将每行指令作为一个字符串传给 as(GAS),使用换行和 TAB 可以将正确且格式良好的代码行传递给汇编器。

例子:

asm ("movl %eax, %ebx\n\t"
     "movl $56, %esi\n\t"
     "movl %ecx, $label(%edx,%ebx,$4)\n\t"
     "movb %ah, (%ebx)");

扩展内联汇编(Extended Asm)

如果在内联代码中操作了一些寄存器,比如你修改了寄存器内容(而之后也没有进行还原操作),程序很可能会产生一些难以预料的情况。因为此时GCC并不知道你已经将寄存器内容修改了。这点尤其是在编译器对代码进行了一些优化的情况下而导致问题。因为编译器注意不到寄存器内容已经被改掉,程序将当作它没有被修改过而继续执行。所以此时我们尽量不要使用这些会产生附加影响的操作,或者当我们退出的时候还原这些操作。否则很可能会造成程序崩溃。可是如果我们必须要这样操作该怎么办呢?
在扩展形式中,我们还可以指定操作数,并且可以选择输入输出寄存器,以及指明要修改的寄存器列表。对于要访问的寄存器,并不一定要要显式指明,也可以留给GCC自己去选择,这可能让GCC更好去优化代码。

asm ( assembler template
        : output operands                /* optional */
        : input operands                 /* optional */
        : list of clobbered registers    /* optional */
);

其中 assembler template 为汇编指令部分。

前两个冒号后面的是操作数(输出、输入):第一个冒号将汇编模板与第一个输出操作数分开,第二个冒号将最后一个输出操作数与第一个输入操作数(如果有)分开。

总结就是:不同类型的操作数使用:分隔,相同类型的操作数使用 , 分隔。

逗号分隔每个组中的操作数,操作数的总数为 10。

寄存器名称前有两个 %,这有助于 GCC 区分操作数和寄存器。操作数有一个 % 作为前缀。

如果没有输出操作数但有输入操作数,那么输出操作数前的冒号不能省

操作符使用格式

每个操作数由一个操作数约束字符串描述,后面小括号中跟 C 语言变量或表达式。

输出操作数有一个约束修饰符 =,这个修饰符表示它是输出操作数并且是只写的。

%0、%1…… %9 它们依次代表 10 个操作数。

限定字符含义
“a”将输入变量放入eax
“b”将输入变量放入ebx
“c”将输入变量放入ecx
“d”将输入变量放入edx
“S”将输入变量放入esi
“D”将输入变量放入edi
“q”将输入变量放入eax,ebx ,ecx ,edx中的一个
“r”将输入变量放入通用寄存器,也就是eax ,ebx,ecx,edx,esi,edi中的一个
“A”放入eax和edx,把eax和edx,合成一个64位的寄存器(uselong longs)
“m”内存变量
“o”操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
“V”操作数为内存变量,但寻址方式不是偏移量类型
“,”操作数为内存变量,但寻址方式为自动增量
“p”操作数是一个合法的内存地址(指针)
“g”将输入变量放入eax,ebx,ecx ,edx中的一个或者作为内存变量
“X”操作数可以是任何类型
“I”0-31 之间的立即数(用于32位移位指令)
“J”0-63 之间的立即数(用于64 位移位指令)
“N”0-255 ,之间的立即数(用于out 指令)
“i”立即数
“n”立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
“=”操作数在指令中是只写的(输出操作数)
“+”操作数在指令中是读写类型的(输入输出操作数)
“f”浮点数
“t”第一个浮点寄存器
“u”第二个浮点寄存器
“G”标准的80387
%该操作数可以和下一个操作数交换位置
#部分注释
*表示如果选用寄存器,则其后的字母被忽略
“&”表示输入和输出操作数不能使用相同的寄存器

#include <stdio.h>

int main()
{
	int a = 10;
	int b = 20;
    int c;
    int d;
	
    asm("movl %3, %%eax \n"
        "movl %%eax, %1 \n"
		:"=b"(c),"=c"(d)
        :"d"(a),"S"(b)
		:"%eax"
        );
    
    printf("d = %d\n", d);
}

运行结果

$ gcc c_inline_asm.c 
$ ./a.out 
d = 20

Tips

两个关键寄存器

ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

pushq    ebp           ;ebp入栈 
movq    esp,ebp        ;因为esp是堆栈指针,无法暂借使用,所以得用ebp来存取堆栈 
问:为什么在反汇编的时候总是有这段代码?

答:首先,在一个函数被调用时,会将旧的ebp进行压栈,此时ebp中存的时调用函数的栈底指针;然后,由于此时的esp已经指向了被调函数的入口,因此,将esp的值赋给ebp,保存函数入口。

指令

STR⚠️

STR{条件} 源寄存器,<存储器地址>

STR指令用亍从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常

用,寻址方式灵活多样,使用方式可参考指令LDR。

指令示例:

STR R0,[R1],#8             ;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。
STR R0,[R1,#8]             ;将R0中的字数据写入以R1+8为地址的存储器中。”
str r1, [r0]                 ;将r1寄存器的值,传送到地址值为r0的(存储器)内存中

LEA⚠️

加载有效地址(load effective address)指令就是lea,他的指令形式就是从内存读取数据到寄存器,但是实际上他没有引用内存,而是将有效地址写入到目的的操作数,就像是C语言地址操作符&一样的功能,可以获取数据的地址。

LEA OPRD1,OPRD2
;OPRD1<--OPRD2

MOV⚠️

本指令将一个源操作数送到目的操作数

MOV OPRD1,OPRD2
;OPRD1<--OPRD2

SUB⚠️

两个操作数的相减

SUB OPRD1,OPRD2
;OPRD1<--OPRD1 - OPRD2

ADD⚠️

两个操作数的相加

ADD OPRD1,OPRD2
;OPRD1<--OPRD1 + OPRD2

CALL⚠️

过程调用指令

CALL OPRD

ENTER与LEAVE⚠️

NTER 指令为被调用过程自动创建堆栈帧。它为局部变量保留堆栈空间,把 EBP 入栈。具体来说,它执行三个操作:

  • 把 EBP 入栈 (push ebp)
  • 把 EBP 设置为堆栈帧的基址 (mov ebp, esp)
  • 为局部变量保留空间 (sub esp, numbytes)

LEAVE 指令结束一个过程的堆栈帧。它反转了之前的 ENTER 指令操作:恢复了过程被调用时 ESP 和 EBP 的值。

MySub PROC
  enter 8,0
  .
  .
  leave
  ret
MySub ENDP

等效于

MySub PROC
  push ebp
  mov ebp, esp
  sub esp, 8
  .
  .
  mov esp, ebp
  pop ebp
  ret
MySub ENDP

函数参数

1、arm64中,参数存放在x0~x7的八个寄存器中
2、如果是浮点就会用浮点寄存器
3、如果超过8个就会用栈传递
4、函数返回值,默认情况下函数的返回值放在x0寄存器中,如果放不下就会利用内存。写入上一个调用栈的内部,用x8寄存器作为参照。


  1. 不严格,不能作为直接判断依据 ↩︎

  2. 若无特殊说明,本文后续语法与命令皆遵循AT&T汇编语法。若为Intel汇编语法,附有⚠️标识 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值