咱们有一个基本的面试题,有些同事可能存在一些误解的情况,本文基于这个题目介绍一下关于aarch64的情况
请说明计数寄存器(PC)和堆栈寄存器(SP)以及链接寄存器(LR)的作用 这个题目很清楚,需要阐述PC,SP和LR的作用,那不清晰的点在哪里呢
如果此问题是arm系列芯片,也就是32位,那么我们这里描述的是
R13,R14,R15
如下:
但是如果是在aarch64系列芯片,也就是64位,那么我们这里描述的是
X29,X30
如下:
值得注意的是,aarch64没有单独的PC寄存器。
主要原因如下,可以从armv8的spec中找到:
The current Program Counter (PC) cannot be referred to by number as if part of the general register file and therefore cannot be used as the source or destination of arithmetic instructions, or as the base, index or transfer register of load and store instructions. The only instructions that read the PC are those whose function it is to compute a PC-relative address (ADR, ADRP, literal load, and direct branches), and the branch-and-link instructions that store a return address in the link register (BL and BLR). The only way to modify the program counter is using branch, exception generation and exception return instructions. Where the PC is read by an instruction to compute a PC-relative address, then its value is the address of that instruction. Unlike A32 and T32, there is no implied offset of 4 or 8 bytes.
这里我们知道三个信息:
也就是说,在aarch64上,pc寄存器可以在函数上可以直接通过偏移计算出来,或者通过adr等加载指令来计算偏移获取pc的值,而跳转可以通过bl来修改pc指针。
至此,我们从官方渠道了解了aarch64的关于pc寄存器的歧义点。
而实际上,在aarch64平台,我们上层使用过程中,仍是可以轻松的获取这些寄存器的值,和arm32相差无几,接下来我简单介绍一下在aarch64平台上这三个寄存器
aarch64的SP寄存器根据异常等级区分,也就是当前owner是ELn,那么SP就是SP_ELn。
我们可以如下示例:
(gdb) info register sp sp 0x7fffffefe0 0x7fffffefe0
sp表示当前函数的堆栈指针,它和x29寄存器是相等的,如下:
(gdb) p/x $x29 $2 = 0x7fffffefe0
对于一个函数,我们在函数的开始,会看到sp会被修改,如下:
0x0000000000400750 <+0>: stp x29, x30, [sp, #-48]!
可以发现sp变小,也就是说,上一级的sp地址应该是sp+48,如下
(gdb) p $sp+48 $5 = (void *) 0x7ffffff010
此时我们到上一级函数load_data中,可以查看如下:
(gdb) p/x $sp $1 = 0x7ffffff010
可以看汇编知道接下来会调用test函数
(gdb) disassemble Dump of assembler code for function load_data: 0x0000000000400674 <+0>: stp x29, x30, [sp, #-48]! 0x0000000000400678 <+4>: mov x29, sp 0x000000000040067c <+8>: str x0, [sp, #24] => 0x0000000000400680 <+12>: ldr x0, [sp, #24] 0x0000000000400684 <+16>: str x0, [sp, #40] 0x0000000000400688 <+20>: ldr x0, [sp, #40] 0x000000000040068c <+24>: ldr x2, [x0] 0x0000000000400690 <+28>: ldr w1, [x0, #8] 0x0000000000400694 <+32>: mov x0, x2 0x0000000000400698 <+36>: bl 0x400750 <test> 0x000000000040069c <+40>: nop 0x00000000004006a0 <+44>: ldp x29, x30, [sp], #48 0x00000000004006a4 <+48>: ret
LR寄存器在gdb中可以直接查看x30的值,如下:
info register x30 x30 0x40071c 4196124
此时我们知道地址0x40071c,我们可以x解析值,这里gdb会帮我们提升为函数,如下
(gdb) x 0x40071c 0x40071c <main+116>: 0xb9401fe0
可以看到,这个地址是main函数+116的栈偏移地址。
此时我们设置断点如下:
(gdb) b *0x40071c Breakpoint 2 at 0x40071c: file ioctl.c, line 29.
此时我们运行,可以看到在断点停下了
Breakpoint 2, main () at ioctl.c:29 29 close(fd);
我们对应代码:
27 load_data(&d); 28 29 close(fd);
可以看到,正好在close上,也就是当前正好是load_data的返回。
通过这里,我们可以很清楚的知道,x30寄存器也就是lr寄存器,其作用是函数返回时的返回地址,这个地址是函数的运行地址。通过x30寄存器,我们可以定位函数位于上级函数的位置。
PC寄存器虽然arm和aarch64在定义上有不同,但是在gdb中行为是一致的,我们可以直接查看pc的值,如下
(gdb) info register pc pc 0x400704 0x400704 <main+116>
这里可以看到,我们
当前的pc值是0x400704,此时我们可以反汇编如下:
(gdb) disassemble Dump of assembler code for function main: 0x0000000000400690 <+0>: stp x29, x30, [sp, #-32]! 0x0000000000400694 <+4>: mov x29, sp 0x0000000000400698 <+8>: mov w1, #0x2 // #2 0x000000000040069c <+12>: adrp x0, 0x400000 0x00000000004006a0 <+16>: add x0, x0, #0x828 0x00000000004006a4 <+20>: bl 0x400510 <open@plt> 0x00000000004006a8 <+24>: str w0, [sp, #28] 0x00000000004006ac <+28>: ldr w0, [sp, #28] 0x00000000004006b0 <+32>: cmp w0, #0x0 0x00000000004006b4 <+36>: b.ge 0x4006c0 <main+48> // b.tcont 0x00000000004006b8 <+40>: mov w0, #0x0 // #0 0x00000000004006bc <+44>: b 0x400710 <main+128> 0x00000000004006c0 <+48>: add x0, sp, #0x10 0x00000000004006c4 <+52>: mov x2, x0 0x00000000004006c8 <+56>: mov x1, #0x6162 // #24930 0x00000000004006cc <+60>: movk x1, #0x8008, lsl #16 0x00000000004006d0 <+64>: ldr w0, [sp, #28] 0x00000000004006d4 <+68>: bl 0x400570 <ioctl@plt> 0x00000000004006d8 <+72>: ldr w0, [sp, #16] 0x00000000004006dc <+76>: ldr w1, [sp, #20] 0x00000000004006e0 <+80>: ldr w2, [sp, #24] 0x00000000004006e4 <+84>: mov w3, w2 0x00000000004006e8 <+88>: mov w2, w1 0x00000000004006ec <+92>: mov w1, w0 0x00000000004006f0 <+96>: adrp x0, 0x400000 0x00000000004006f4 <+100>: add x0, x0, #0x838 0x00000000004006f8 <+104>: bl 0x400560 <printf@plt> 0x00000000004006fc <+108>: add x0, sp, #0x10 0x0000000000400700 <+112>: bl 0x400674 <load_data> => 0x0000000000400704 <+116>: ldr w0, [sp, #28] 0x0000000000400708 <+120>: bl 0x400530 <close@plt> 0x000000000040070c <+124>: mov w0, #0x0 // #0 0x0000000000400710 <+128>: ldp x29, x30, [sp], #32 0x0000000000400714 <+132>: ret
可以发现,这里有一个箭头,箭头地址就是pc的值。
我们可以知道,pc的值有三种修改方式
再加上pc寄存器有一个特性,指向的是下一条语句的运行,所以pc的值是即将运行的代码。
至此,我们了解了pc寄存器的值。
至此,我们应该能够完全的理解aarch64的pc,sp和lr三个寄存器的完全解释了。