裸函数naked解析 编写 Prolog/Epilog 代码的注意事项

先分享一个案例:

 1 #include <stdio.h>
 2 
 3 __declspec(naked) void Test()
 4 {
 5     int x;
 6     x = 3;
 7     __asm ret;
 8 }
 9 
10 int main(int argc, char* argv[])
11 {
12     int x = 1;
13     Test();
14     printf("%d
",x);
15     return 0;
16 }

猜猜输出什么?输出3,而不是1。

看下反汇编代码:

裸函数naked解析
编写 Prolog/Epilog 代码的注意事项

有疑问先留着。下面讲解下naked:

MSDN中关于naked关键字的介绍:

For functions declared with the naked attribute, the compiler generates code without prolog and epilog code. You can use this feature to write your own prolog/epilog code sequences using inline assembler code. Naked functions are particularly useful in writing virtual device drivers. Note that thenaked attribute is only valid on x86, and is not available on x64 or Itanium.

我们知道VC++和gcc都支持naked函数,即所谓的“裸函数”,naked 特性仅适用于 x86和ARM,并不用于 x64 。关于自己编写prolog 和 epilog 代码,在后面有讲到。

VC++的声明语法:__declspec(naked)

gcc的声明语法:__attribute__((naked))

因为编译器不会生成入口代码和退出代码,所以写naked函数的时候要分外小心。进入函数代码时,父函数仅仅会将参数和返回地址压栈,亦即只有esp寄存器和eip寄存器会发生变化。

一般来说,使用naked函数时需要注意以下问题:(以VC++编译器为例)

1、函数必须显式返回。

一般通过__asm ret的内嵌汇编指令返回。

2、不可以通过任何方式使用局部变量。

若声明一个局部变量,并在代码中为其赋值,则会更改父函数中相应位置的局部函数的值。

3、只能通过esp引用参数。

因为子函数继承了父函数的ebp寄存器,所以只能通过esp引用参数。

4、naked 属性仅与函数的定义相关,不能在函数原型中指定。不能用于函数指针,不能用于数据定义。

官方给出的naked的规则限制:

  • return 语句。

  • 不允许结构化异常处理和 C++ 异常处理构造,因为它们必须在堆栈帧中展开。

  • 出于同一原因,禁止任何形式的 setjmp

  • 禁止使用 _alloca 函数。

  • 但是,嵌套的范围中可能有初始化的数据。

  • 不建议使用帧指针优化(/Oy 编译器选项),但会自动为裸函数将其取消。

  • 但是,可以在嵌套的块中声明对象。

  • naked 关键字。

  • 例如:

     1 // nkdfastcl.cpp
     2 // compile with: /c
     3 // processor: x86
     4 __declspec(naked) int __fastcall  power(int i, int j) {
     5    // calculates i^j, assumes that j >= 0
     6 
     7    // prolog
     8    __asm {
     9       push ebp
    10       mov ebp, esp
    11       sub esp, __LOCAL_SIZE
    12      // store ECX and EDX into stack locations allocated for i and j
    13      mov i, ecx
    14      mov j, edx
    15    }
    16 
    17    {
    18       int k = 1;   // return value
    19       while (j-- > 0) 
    20          k *= i;
    21       __asm { 
    22          mov eax, k 
    23       };
    24    }
    25 
    26    // epilog
    27    __asm {
    28       mov esp, ebp
    29       pop ebp
    30       ret
    31    }
    32 }

堆栈帧布局:

此示例显示了可能出现在 32 位函数中的标准 prolog 代码:

push        ebp                ; Save ebp
mov         ebp, esp           ; Set stack frame pointer
sub         esp, localbytes    ; Allocate space for locals
push        <registers>        ; Save registers

下面是相应的 epilog 代码:

pop         <registers>   ; Restore registers
mov         esp, ebp      ; Restore stack pointer
pop         ebp           ; Restore ebp
ret                       ; Return from function

ebp 的偏移量。

__LOCAL_SIZE :

此符号用于在自定义 prolog 代码中的堆栈帧上为局部变量分配空间。

例如:

mov        eax, __LOCAL_SIZE           ;Immediate operand--Okay
mov        eax, [ebp - __LOCAL_SIZE]   ;Error

以下包含自定义 prolog 和 epilog 序列的裸函数的示例在 prolog 序列中使用 __LOCAL_SIZE 符号:

// the__local_size_symbol.cpp
// processor: x86
__declspec ( naked ) int main() {
   int i;
   int j;

   __asm {      /* prolog */
      push   ebp
      mov      ebp, esp
      sub      esp, __LOCAL_SIZE
      }
      
   /* Function body */
   __asm {   /* epilog */
      mov      esp, ebp
      pop      ebp
      ret
      }
}

最初的案例现在看起来简单多了,编译器不主动加prolog 和 epilog 代码,子函数用父函数的ebp,所以这里修改了main函数的栈,后面结果出错了。后面的ret是必须写蛤,要自己维持栈平衡。