访存指令是ARM64汇编语言中的一部分,涵盖了数据访问和内存操作的指令。这些指令使处理器能够与内存进行交互,包括读取数据、写入数据以及执行其他内存相关的操作。理解这些指令对于理解处理器如何与内存交互以及如何管理数据至关重要。
访存指令介绍两个:
-
ldr
-
str
LDR
ldr 指令的意思是 Load Register,将寄存器里面的值当成地址,访问该内存地址储存的值,将这个值拿出来放到另一个寄存器。比如:
LDR X8, [X21]
[X21] 类似数组写法,将内存看作数组,就是将 Mem[X21] 的值给 X8。
查看手册,LDR 有3种形式:
-
LDR (immediate)
-
LDR (literal)
-
LDR (register)
其实差别不大,在 IDA 中没啥区别,只需要了解它是在读取内存即可。
拿 LDR (immediate) 的格式来说:
imm9 是描述的偏移,范围是 -256 到 255,刚好占据9位,有一位是符号位。
第 10 与 11 bit位是表示的是内偏移还是外偏移。
STR
STR 与 LDR 对应,表示的意思是Store Register:
STR X10, [SP]
将X10的值存到 SP 指向的内存地址上。
STR 有2种形式:
-
STR (immediate)
-
STR (register)
后续行为
访存指令还会有一些后续行为,就是类似于++i
与i++
。
内偏移写法,比如:
E1 17 40 F9 LDR X1, [SP,#0x28]
这里计算地址要将 SP 的值加上 0x28,当成内存地址,然后获取地址上的值,赋值给 X1。
还有一种带感叹号的形式:
20 4C 40 F8 LDR X0, [X1,#4]!
感叹号的作用就是说,当指令执行完后,X1 的值也需要加上 4。
还有一种外偏移的形式:
20 44 40 F8 LDR X0, [X1],#4
这种写法,也是当指令执行完后,X1 的值也需要加上 4。不过,在访存的时候,不会先加4,注意与带感叹号写法的区别。
内偏移与外偏移不会同时存在,就算想做也不行,因为指令位不够。
其他访存指令
与 LDR 相似的,还有 LDUR 等指令,也是访问内存,可以理解为 LDR 的特殊情况。
LDR指令:将数据从内存中取出来,存放到寄存器中。
LDUR指令:将内存中负数的数据取出来,并存放到寄存器中。
LDP指令:表示出栈指令
STR指令:将数据从寄存器中读出来,存储到内存中。
STUR指令:将寄存器中的负数数据读取出来,存放到内存中。
STP指令:表示入栈指令。
比如,在 IDA 中 patch ldr 指令的时候,就有可能会生成 ldur 指令。查看手册发现这两者的指令格式非常相似,只有一个 bit 位的区别。
理解全局变量
看一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
uint64_t x;
int main()
{
x = 0x123456789a;
getchar();
return 0;
}
查看汇编代码:
.text:00000000000006F4 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000000006F4 EXPORT main
.text:00000000000006F4 main ; DATA XREF: LOAD:0000000000000438↑o
.text:00000000000006F4 ; .got:main_ptr↓o
.text:00000000000006F4
.text:00000000000006F4 var_s0= 0
.text:00000000000006F4
.text:00000000000006F4 ; __unwind {
.text:00000000000006F4 FD 7B BF A9 STP X29, X30, [SP,#-0x10+var_s0]!
.text:00000000000006F8 FD 03 00 91 MOV X29, SP
.text:00000000000006FC 08 00 00 B0 ADRP X8, #x_ptr@PAGE
.text:0000000000000700 08 F1 47 F9 LDR X8, [X8,#x_ptr@PAGEOFF]
.text:0000000000000704 49 13 8F D2 C9 8A A6 F2 49 02 C0 F2 MOV X9, #0x123456789A
.text:0000000000000710 09 01 00 F9 STR X9, [X8]
.text:0000000000000714 C7 FF FF 97 BL .getchar
.text:0000000000000714
.text:0000000000000718 E0 03 1F 2A MOV W0, WZR
.text:000000000000071C FD 7B C1 A8 LDP X29, X30, [SP+var_s0],#0x10
.text:0000000000000720 C0 03 5F D6 RET
.text:0000000000000720 ; } // starts at 6F4
.text:0000000000000720
.text:0000000000000720 ; End of function main
.text:0000000000000720
.text:0000000000000720 ; .text ends
.text:0000000000000720
从 00000000000006FC
地址开始分析:ADRP X8, #x_ptr@PAGE
就是先将 PC 的地址做个页对齐,再加上某个偏移量。但是这里 IDA 已经帮我们计算好了,最终的值就是 #x_ptr@PAGE
,所以该指令的意思就是将这个值赋值给 X8 寄存器。
x_ptr 是一个地址,从名字也可以看出,它是 x 变量的一个指针,指向的地址储存了 x 的值。
下一条指令:LDR X8, [X8,#x_ptr@PAGEOFF]
将X8 + x_ptr@PAGEOFF
的值处的地址储存的值赋值给 X8,从上一条指令我们知道,X8 的值是 x_ptr@PAGE
,所以最终X8的值是 x_ptr@PAGE + x_ptr@PAGEOFF
处储存的值。
那么x_ptr@PAGE
与 x_ptr@PAGEOFF
这两个玩意到底是啥呢?
我们在这个符号的位置,按下 Q 快捷键,就能知道它真实的数字了:
.text:00000000000006FC 08 00 00 B0 ADRP X8, #0x1000
.text:0000000000000700 08 F1 47 F9 LDR X8, [X8,#0xFE0]
所以,最后的结果是将 0x1FE0 处储存的值放到了 X8 寄存器中。
我们去看 0x1FE0 处的内容:
.got:0000000000001FE0 08 20 00 00 00 00 00 00 x_ptr DCQ x
所以,就是将 0x0000000000002008 放入 X8 寄存器中。
看下一个指令:MOV X9, #0x123456789A
将0x123456789A
的值放入 X9 寄存器。
看下一个指令:STR X9, [X8]
将 X9 寄存器的值,放入内存中,就是将 0x123456789A
的值,放入 0x0000000000002008
这个地址。
但是需要注意,最终的地址肯定是需要 rebase 的,因为 adrp 指令计算了 pc 的值,静态分析时,我们的 pc 值相当于 0,动态调试的时,程序的一些地方需要重定位,肯定会改变一些值。
现在可以考虑一下:
.got:0000000000001FE0 08 20 00 00 00 00 00 00 x_ptr DCQ x
当程序运行时,这里储存的地址会不会变化?
我们调试看一下:
当这两条指令执行完之后,X8 寄存器的值为 0x00000056CBF43008
。
与静态分析时的地址相比较 0x0000000000002008
,是页对齐的。
此时0x00000056CBF43008
储存的内容为:
赋值完成后,查看
后面,看到类似这种指令:
globalvar:00000056CBF41750 ADRP X8, #off_56CBF42FE0@PAGE
globalvar:00000056CBF41754 LDR X8, [X8,#off_56CBF42FE0@PAGEOFF]
要能想到这是访问全局变量。
got 储存了全局变量的地址,程序加载时对这个地址进行重定位,运行时给这个地址填入全局变量的值。
二手的程序员
红日初升,其道大光。河出伏流,一泻汪洋。潜龙腾渊,鳞爪飞扬。乳虎啸谷,百兽震惶。鹰隼试翼,风尘翕张。奇花初胎,矞矞皇皇。干将发硎,有作其芒。天戴其苍,地履其黄。纵有千古,横有八荒。前途似海,来日方长。
公众号