几个概念
立即数
1)把数据转换成二进制形式,从低到高写成 4位1组的形式,最高位一组不够4位的前面补0
2)数1的个数,如果大于8个【可能也是立即数,取反】不是立即数,如果小于等于8个 进行下面步骤
3)如果数据中间有连续的大于等于24个0,循环左移2的倍数,使高位全为0
4)找到最高位的1,去掉前面的最大偶数个0
5)找到最低位的1,去掉后面偶数个0
6)数剩下的位数,如果小于等于8位,那么这个数就是个立即数,反之就不是立即数
条件码
条件码:本条指令的执行,依赖于上一个指令的执行结果
寄存器
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
上图描述的是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汇编,两者语法规则存在异同。
架构 | 指令 |
---|---|
INTEL | MOV EAX,EBX |
AT&T | mov %ebx,%eax |
大小写1
INTEL格式的指令使用大写字母,而AT&T格式的使用小写字母
操作数赋值方向
在INTEL语法中,第一个表示目的操作数,第二个表示源操作数,赋值方向从右向左。
AT&T语法第一个为源操作数,第二个为目的操作数,方向从左到右。
前缀
在 INTEL 语法中寄存器和立即数不需要前缀;AT&T 中寄存器需要加前缀“%” ;立即数需要加前缀“$” 。
架构 | 指令 |
---|---|
INTEL | MOV EAX,1 |
AT&T | movl $1,%eax |
架构 | 指令 | ||
---|---|---|---|
INTEL | CALL FAR SECTION:OFFSET | JMP FAR SECTION:OFFSET | RET FAR SATCK_ADJUST |
AT&T | lcall $secion:$offset | ljmp $secion:$offset | lret $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不需要加前缀“&” 。
架构 | 指令 |
---|---|
INTEL | foo,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。
INTEL | AT&T |
---|---|
MOV AL,BL | movb %bl,%al |
MOV AX,BX | movw %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“,分别为 ljmp
,lcall
,与之相应的返回指令伪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寄存器作为参照。