在之前的文章中,我们已经简单的了解了64位的一些参数的知识,下面我们从不同的情况对64位的堆栈与函数做详细的讲解。
首先,对于64位的函数,他的参数并不都是以push压栈的方式进行传递的,他的前4个参数分别通过RCX,RDX,R8,R9这几个寄存器进行传递(如图)
从图中的函数可以发现,64位的函数是不会对参数进行push的,而是将前4个参数传递给了RCX-R9这4个寄存器。从参数窗口,可以轻易的看到这些参数的数值。
那么在这种情况下如何分辨具体用到了哪些参数呢?
首先在函数调用处进行观察,一般来说,函数的参数在调用处附近会传递给RCX-R9这4个寄存器,并在函数内部进行调用。所以观察在函数调用前,看哪些寄存器被进行赋值。
其次在被调用函数头部进行观察,看4个寄存器中的哪些在函数中对其他内存进行赋值(如图)
图中的RDX,R8,R9都在函数头部被使用,而RCX则在被使用前被其他地址进行了赋值,所以说这个函数只需要传递RDX-R9三个参数即可,而RCX并不会影响该函数的调用。当然,这种情况也是非常少见的,通常RCX的使用率会远高于其他的三个寄存器。
以上是在函数参数不多于4个的情况下,函数的参数传递情况。那么当函数的参数超过4个以后,参数又如何进行传递呢?(如图)
图中是某寻路函数,在函数被调用前,我们可以看到出了RCX-R9寄存器意外,还有rsp+20,rsp+28,……,rsp+40被赋值,这些被赋值的堆栈地址,就是函数第四个参数以后的其他的参数。
在rsp+20前面还有20字节,是作为预留的4个8字节存在的,有时候会在函数内部用来传递RCX-R9这4个参数,而在调用函数时通常是无需在意这20字节的。
当然,64位的函数在单步进入函数内部时也需要push一个8字节的返回地址,这一点和32位相同(如图)
这时的第五个参数则变为了rsp+28,后面的参数地址依次+8,而前面的4个参数不变,依然为RCX-R9。
可以将64为函数的参数传递总结为下图(如图)
以上为64为函数的参数与堆栈的关系,那么在函数执行的过程中如果遇到了堆栈地址应该如何分析呢?这里也分为几种情况。
参数的传递
参数的传递相对局部变量会简单很多,通常我们只需要将堆栈地址计算到函数头部,就可以得出具体是由那个参数传递的(如图)
022E1E88处的r14d来源于[rbp+148],而rbp+148计算到头部为rsp+40,在头部到022E1E88处没有任何代码对这个地址赋值,也就是说明这个地址里的值是来源于上一层传进来的参数(如图)
执行到返回后我们可以看到22F1F5B处[rsp+38]被r15b赋值,这也验证了我们的之前的推测。
局部变量的传递
局部变量的传递也分为很多种,第一种是通过rbp+N的这种方式进行传递的,这里的N可能为正值也可能为负值(如图)(如图)
图中的rax来源于[RBP+37],而RBP在头部来源于rax-5F,经过计算,rbp+37的地址在函数头部为rsp-28,这说明rbp+37其实是一个局部变量。于是我们在函数内部进行分析,并得到1E5C57D处的mov qword ptr [rbp + 0x37], rsi
这是最简单的,也是最常见的局部变量传递。
第二种是局部变量以参数的形式传递到函数中,并在函数中被赋值(如图)(如图)
图中1A497B1处的rcx来源于[rsp+70],而rsp+70计算到头部为rsp+8,看似是来源于第一个参数的,但是并没有参数向这个地址中传递数值,而在头部下断发现rsp+8里也的确没有被写入任何数值。这说明[rsp+70]其实是一个局部变量。
在1A49797处我们发现rsp+70的地址传给了rdx,而经过下面这个函数后被赋值,这说明rsp+70做为结构体参数被传入到了函数中,并且在函数内部被进行赋值。以下是赋值处的代码(如图)
以上就是参数和局部变量常见的传递方式。在64位代码中,局部变量和参数分析起来非常简单,因为大部分的函数都不会对堆栈造成影响。但是参数的分析则麻烦一些,因为参数的传递一般不会用到push,而好处则是当参数个数较少的时候,我们不会因为忽视堆栈的变化导致堆栈不平衡,进而让程序崩溃。