结合ARM相关文档和在飞腾机器上使用gdb调试实际程序来研究ARM的指令和运行时栈帧布局。主要参考了三篇文档。
1. Procedure Call Standard for the ARM 64-bit Architecture。参考其中的过程调用标准和运行时栈帧布局。
2. ARMv8 Instruction Set Overview。参考其中的指令概述。
3. ARM Compiler Migration and Compatibility Guide。参考其中ARM汇编与GNU汇编格式的比较。
在文章1中,对ARM架构下运行时栈帧布局如图1所示。
图1 ARM运行时栈帧布局
其中,FP(x29)寄存器保存栈帧地址,LR(x30)保存当前过程的返回地址。栈是从高地址向低地址生长。为验证图中的布局形式,在飞腾机器上安装gdb,通过调试一个示例程序,来研究ARM的指令特点和栈帧结构。示例程序如图2所示,函数TestParam定义了两个局部变量,分别为数组和标量类型。
图2 示例程序
使用gdb调试图2中代码所产生的程序,然后再反汇编函数TestParam,可以得到如下结果。
=> 0x0000000000400680 <+0>:stpx29, x30, [sp,#-64]!
0x0000000000400684 <+4>:movx29, sp
0x0000000000400688 <+8>:strw0, [x29,#28]
0x000000000040068c <+12>:strw1, [x29,#24]
0x0000000000400690 <+16>:strw2, [x29,#20]
0x0000000000400694 <+20>:strw3, [x29,#16]
0x0000000000400698 <+24>:adrpx0, 0x411000 <[email protected]>
0x000000000040069c <+28>:addx0, x0, #0x38
0x00000000004006a0 <+32>:ldrx1, [x0]
0x00000000004006a4 <+36>:strx1, [x29,#56]
0x00000000004006a8 <+40>:movx1, #0x0 // #0
0x00000000004006ac <+44>:ldrw1, [x29,#28]
0x00000000004006b0 <+48>:ldrw0, [x29,#24]
0x00000000004006b4 <+52>:addw0, w1, w0
0x00000000004006b8 <+56>:strw0, [x29,#48]
0x00000000004006bc <+60>:ldrw1, [x29,#20]
0x00000000004006c0 <+64>:ldrw0, [x29,#16]
0x00000000004006c4 <+68>:subw0, w1, w0
0x00000000004006c8 <+72>:strw0, [x29,#52]
0x00000000004006cc <+76>:ldrw1, [x29,#20]
0x00000000004006d0 <+80>:ldrw0, [x29,#16]
0x00000000004006d4 <+84>:addw0, w1, w0
0x00000000004006d8 <+88>:strw0, [x29,#44]
0x00000000004006dc <+92>:ldrw1, [x29,#48]
0x00000000004006e0 <+96>:ldrw2, [x29,#52]
0x00000000004006e4 <+100>:adrpx0, 0x400000
0x00000000004006e8 <+104>:addx0, x0, #0x808
0x00000000004006ec <+108>:ldrw3, [x29,#44]
0x00000000004006f0 <+112>:bl0x400530
0x00000000004006f4 <+116>:adrpx0, 0x411000 <[email protected]>
0x00000000004006f8 <+120>:addx0, x0, #0x38
0x00000000004006fc <+124>:ldrx1, [x29,#56]
0x0000000000400700 <+128>:ldrx0, [x0]
0x0000000000400704 <+132>:eorx0, x1, x0
0x0000000000400708 <+136>:cmpx0, xzr
0x000000000040070c <+140>:b.eq0x400714
0x0000000000400710 <+144>:bl0x400500 <__stack_chk_fail@plt>
0x0000000000400714 <+148>:ldpx29, x30, [sp],#64
0x0000000000400718 <+152>:ret
该程序在运行时的栈帧如图3所示。
以下是反汇编指令的解释以及其对栈中内容的影响。
=> 0x0000000000400680 <+0>:stpx29, x30, [sp,#-64]!
0x0000000000400684 <+4>:movx29, sp
指令stp把一对值x29和x30放到SP-64的地址(7ffffff370)中去。此时的SP是旧SP,其值为7ffffff3b0。值得注意的是,这条语句同时完成了SP的自减运算,也就是执行之后,SP的值也变成了7ffffff370。第二条指令把FP的值设置为与SP的值相同。
0x0000000000400688 <+8>:strw0, [x29,#28]
0x000000000040068c <+12>:strw1, [x29,#24]
0x0000000000400690 <+16>:strw2, [x29,#20]
0x0000000000400694 <+20>:strw3, [x29,#16]
这4条指令把保存在参数传递寄存器中的4个参数保存到栈中。如图2中的w0, w1, w2, w3所示。
0x0000000000400698 <+24>:adrpx0, 0x411000 <[email protected]>
0x000000000040069c <+28>:addx0, x0, #0x38
0x00000000004006a0 <+32>:ldrx1, [x0]
0x00000000004006a4 <+36>:strx1, [x29,#56]
0x00000000004006a8 <+40>:movx1, #0x0 // #0
这5条指令是用于安全保障的。因为函数TestParam中声明了一个数组,因此有受到缓冲区溢出攻击的危险。在其他平台下或者之前版本中,需要在编译时显式使用-fstack-protector选项,才会增加这样的安全保障指令。而在飞腾ARM配置的编译器中,默认就增加了。
其主要思路是在编译时生成一个随机化的值,如图中的_stack_guard保存在bss段中。在开始执行函数体时,把它从bss段中取出,放在栈的底部。然后执行函数。若有针对数组e的缓冲区溢出攻击,则_stack_guard就会被改写。在函数执行结束时,再把栈底部的值和bss段中的原始值相比较,若两者不同,就说明有攻击行为发生。
这5条指令的功能就是从bss段中把_stack_guard的值放到栈的底部。需要注意的是,在查找时使用了相对寻址指令adrp。
0x00000000004006ac <+44>:ldrw1, [x29,#28]
0x00000000004006b0 <+48>:ldrw0, [x29,#24]
0x00000000004006b4 <+52>:addw0, w1, w0
0x00000000004006b8 <+56>:strw0, [x29,#48]
0x00000000004006bc <+60>:ldrw1, [x29,#20]
0x00000000004006c0 <+64>:ldrw0, [x29,#16]
0x00000000004006c4 <+68>:subw0, w1, w0
0x00000000004006c8 <+72>:strw0, [x29,#52]
这8条指令是使用形式参数进行运算,并把结果保存在数组中。数组中只有两个元素,被放置在靠近栈底的位置。
0x00000000004006cc <+76>:ldrw1, [x29,#20]
0x00000000004006d0 <+80>:ldrw0, [x29,#16]
0x00000000004006d4 <+84>:addw0, w1, w0
0x00000000004006d8 <+88>:strw0, [x29,#44]
这4条指令的作用是计算出f的值,并把它保存到栈中。
0x00000000004006dc <+92>:ldrw1, [x29,#48]
0x00000000004006e0 <+96>:ldrw2, [x29,#52]
0x00000000004006e4 <+100>:adrpx0, 0x400000
0x00000000004006e8 <+104>:addx0, x0, #0x808
0x00000000004006ec <+108>:ldrw3, [x29,#44]
0x00000000004006f0 <+112>:bl0x400530
这6条指令是负责准备参数寄存器x0、w1、 w2和w3的值,并调用printf。x0中存放的是指向格式字符串的指针。
0x00000000004006f4 <+116>:adrpx0, 0x411000 <[email protected]>
0x00000000004006f8 <+120>:addx0, x0, #0x38
0x00000000004006fc <+124>:ldrx1, [x29,#56]
0x0000000000400700 <+128>:ldrx0, [x0]
0x0000000000400704 <+132>:eorx0, x1, x0
0x0000000000400708 <+136>:cmpx0, xzr
0x000000000040070c <+140>:b.eq0x400714
0x0000000000400710 <+144>:bl0x400500 <__stack_chk_fail@plt>
这8条指令是比较_stack_guard的值与存放在bss段中的值是否相等,若相等,在跳到400714,继续执行TestParam函数,否则跳到_stack_chk_fail函数,处理缓冲区溢出发生的情况。
0x0000000000400714 <+148>:ldpx29, x30, [sp],#64
0x0000000000400718 <+152>:ret
这2条指令从栈中恢复x29和x30的值,并返回。需要注意的是ldp指令执行之后,sp的值自动增加了64。