首先看一下源码
#include
#include
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}
这个main函数
的逻辑比较清晰明了,输入一个key值
,如果key1()+key2()+key3()) == key
就能获得flag。但是key1、key2、key3
的值都是用汇编代码写的,而且是ARM汇编(通过leg.asm
文件发现里面寄存器都是表示成r0
什么的) 。X86采用eax
作为返回值,ARM使用r0
作为返回值。
首先来看key1
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
和返回值有关的是下面这两句
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
由于ARM采用了流水线机制,当正确读取了PC的值时,该值为当前指令地址加8个字节,也就是key1 = 0x00008cdc + 0x8
在ARM中,程序计数器R15
又被记作PC
。
ARM7系列处理器中每条指令分取指(IF)、译码(ID)、执行(EX)三个阶段,分别在不同的功能部件上依次独立完成。取指部件完成从存储器装载一条指令,通过译码部件产生下一周期数据路径需要的控制信号,完成寄存器的解码,再送到执行单元完成寄存器的读取、ALU运算及运算结果的写回,需要访问存储器的指令完成存储器的访问。
流水线上虽然一条指令仍需3个时钟周期来完成,但通过多个部件并行,使得处理器的吞吐率约为每个周期一条指令,提高了流式指令的处理速度
下面通过两个图来说明流水线机制,第一个图为非流水时空图,第二个图为流水时空图
可以看到,在非流水机制中,6个机器时钟周期执行了2条指令。流水机制中6个时间周期执行了4条指令。PC总是指向“正在取指”的指令,从图中可以看到,执行指令1
的时候,正在对指令3
进行取址,所以,PC值 = 当前程序执行位置 + 8
。
再来看一下key2
(gdb) disass key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.
BX 指令跳转到指令中指定的目标地址。寄存器中为跳转的目标地址,当寄存器的bit[0]
为0
时,目标地址处的指令为ARM指令;当寄存器的bit[0]
为1
时,目标地址处的指令为Thumb指令,每条指令2byte,所以PC值 = 当前程序执行位置 + 4
。
0x00008cfc <+12>: add r6, pc, #1
这一行执行r6 = 0x00008d04 + 0x1 = 0x00008d05
0x00008d00 <+16>: bx r6
从而bx r6就跳转thumb
模式下。
0x00008d04 <+20>: mov r3, pc
这一行执行r3 = 0x00008d04 + 0x4
0x00008d06 <+22>: adds r3, #4
这一行执行r3 = r3 + 0x4 = 0x00008d04 + 0x4 + 0x4 = 0x00008d0c
所以返回值r0(r0的值等于r3的值,0x00008d10 <+32>: mov r0, r3
)
所以key2=0x00008d04 + 0x4 + 0x4
最后看key3
(gdb) disass key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.
0x00008d2c <+12>: mov r0, r3
这一行将r3的值给r0,再往上找,0x00008d28 <+8>: mov r3, lr
这一行将lr的值给r3。
寄存器R14又被称为连接寄存器(Link Register, LR),在ARM体系中具有下面两种特殊作用:
(1)存放当前子程序的返回地址。
(2)当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。
这里应该是子程序的返回地址,去main函数中找调用key3函数的地方
0x00008d7c <+64>: bl 0x8d20 <key3>
0x00008d80 <+68>: mov r3, r0
在ARM体系中,B和BL指令为跳转指令,二者的不同之处在于,B指令
仅仅执行跳转操作;BL指令
同时还将下条指令的地址保存到LR寄存器中。所以key3
的值为0x00008d80
最后可以得到
key1 = 0x00008cdc + 0x8
key2 = 0x00008d04 + 0x4 + 0x4
key3 = 0x00008d80
key = key1 + key2 + key3
key = 108400
这道题主要是考ARM汇编相关的知识,如果学过这方面的知识,那么这道题非常简单,我对这方面不是很熟,如果有什么不对的地方,还请指出来。