测试编译器下限之不同大小返回值的处理

2014/08/10 Reverse_Engineering

写在前面的话

  编译器会根据函数返回类型大小(sizeof操作符进行4字节对齐后的值)不同进行不同的操作。如果该值为4字节,那么默认会返回到EAX寄存器中,如果该值为8字节,则通常使用EAX寄存器装载结果的低4字节,EDX寄存器装载结果的高4字节。如果该值超过8,则根据编译器的不同会有不同的行为,在剩余寄存器足够用的情况下,通常会使用这些寄存器来传值,如果编译器发现现有寄存器无法满足返回值长度要求,那么就会在内存中开辟一块相对安全的栈区域用于存储该值,赋值完毕后,将起始位置指针传递给EAX。一般情况下,返回大小超过8字节的行为并不恰当,如果在设计不良的编译器中很有可能造成返回地址覆盖,也极为少见。(基于32位 MSVC)

  • 对于4字节返回值,例如int,众所周知,返回值存在EAX
  • 对于8字节返回值,例如longlong,众所周知,返回值存于EDX:EAX
  • 对于8字节以上返回值,虽然编程方法错误(因为有些不良编译器可能直接返回栈地址),但是出于理论研究,还是想看看编译器怎么考虑。

  对于20字节返回值,做个测试,如果从头单步调试的话,你会发现这句居然在父函数未使用的栈空间(通过ebp得到)分配了一段空间。返回值也不存在那些个寄存器里了,而是填充完毕栈以后,将首地址返回给EAX,对16字节、12字节、8字节测试,效果同上,栈空间分配减少字节而已
  通常,对于8字节以上的返回值,都不会通过按值传递来返回,而是按引用传递,这样做一方面可以避免编译器返回局部变量地址,另一方面用地址返回速度较快,不用一个个值拷贝。考虑到函数结束后,栈应该由系统收回,所以不应该再使用,因此不能让其传递为当前函数栈变量;基于上述2点,需要返回的地址,需要在调用参数时由父函数提供,这也是最正规的,不会产生bug的思路,举个例子:

struct pt
{
        int x;
        int y;
        int z;
}
const pt& Getpt(pt& src);
Show Disqus Comments

Search

    Table of Contents