XSLT存档  

不及格的程序员-八神

 查看分类:  ASP.NET XML/XSLT JavaScripT   我的MSN空间Blog
作者:不及格的程序员-八神
 
 
函数是写程序中被广泛使用的通用模块,目前流行的主要语言或平台一般都支持的可变数目参数,可实现方式大不一样。
就是说当函数被调用时,参数的个数是不固定的。
 

C/C++的实现

该平台实现可变数目参数的函数,最复杂,它靠调用者将参数压栈传递给它,同时在调用完成函数时,调用者负责清理栈,将栈空间恢复,维持栈平衡。
经典的C语言函数printf就支持可变数目的参数,因为它约定了由调用者清理栈。这种约定很重要,不然调用者清理栈,函数本身也清理栈,栈的结构就会被破坏.

因为只有调用者知道要传给它多少个参数.如果由函数本身清理,就会出现问题,它根本不知道外层都有哪些栈空间传递过来.

x86平台下有几种约定方式,c调用协定(支持) 标准调用协定 快速调用协定 this调用协定 clr调用协定 x64调用协定(支持)
 

.NET 平台

以C#为例,它也可以通过在函数中使用params关键字实现可变数目参数(其实是假的,其实即时编译器会成类似数组的参数传过进去),它使用的是快速调用协定,通过cx与dx寄存器传递参数首地址。

由clr运行时的即时编译器来处理计算函数参数的个数与地址。
总之它是假的,并不是真正用像"c调用协定"的方式使用栈空间操作。
 
WEB平台的javascript

更简单js解析器会在函数范围内维护一个对象(类似数组) ,它包含了全部函数的使用的参数...

不过现在比较时兴将javascript编译成本地码的js引擎,我猜它们的实现方式也应该类似以上两种形式,每家都有各自的高招.


 

非托管调用约定

 

本文内容

  1. 平台默认调用约定
  2. 在托管 P/Invoke 声明中指定调用约定
  3. 在早期 .NET 版本中指定调用约定
  4. 另请参阅

调用约定将描述有关方法参数和返回值如何在调用方与调用方法之间传递的低级别详细信息。

重要的是,P/Invoke 声明中声明的非托管调用约定需要与本机实现使用的非托管调用约定相匹配。 非托管调用约定中的不匹配会导致数据损坏和严重崩溃,这需要低级别的调试技能进行诊断。

平台默认调用约定

大多数平台都会使用一个标准的调用约定,因此在大多数情况下,都不需要显式指定调用约定。

对于 x86 体系结构,默认的调用约定时特定于平台的。 Stdcall(“标准调用”)是 Windows x86 上的默认调用约定,大多数 Win32 API 都使用该调用约定。 Cdecl 是 Linux x86 上的默认调用约定。 源自 Unix 的开源库的 Windows 端口通常使用 Cdecl 调用约定,即使在 Windows x86 上也是如此。 必须在 P/Invoke 声明中显式指定 Cdecl 调用约定,以便与这些库实现互操作。

对于非 x86 体系结构,Stdcall 和 Cdecl 调用约定都被视为标准的平台默认调用约定。

在托管 P/Invoke 声明中指定调用约定

调用约定由 System.Runtime.CompilerServices 命名空间中的类型或其组合指定:

显式指定的调用约定的示例:

C#
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

// P/Invoke declaration using SuppressGCTransition calling convention.
[LibraryImport("kernel32.dll")]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvSuppressGCTransition) })]
extern static ulong GetTickCount64();

// Unmanaged callback with Cdecl calling convention.
[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })]
static unsafe int NativeCallback(void* context);

// Method returning function pointer with combination of Cdecl and MemberFunction calling conventions.
static unsafe delegate* unmanaged[Cdecl, MemberFunction]<int> GetHandler();

在早期 .NET 版本中指定调用约定

.NET Framework 和 .NET 5 之前的 .NET 版本仅限于一小部分可由 CallingConvention 枚举描述的调用约定。

显式指定的调用约定的示例:

C#
using System.Runtime.InteropServices;

// P/Invoke declaration using Cdecl calling convention
[DllImport("ucrtbase.dll", CallingConvention=CallingConvention.Cdecl)]
static void* malloc(UIntPtr size);

// Delegate marshalled as callback with Cdecl calling convention
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void Callback(IntPtr context);

另请参阅

 


 

NSCOOKIES

提供一些OC与SWIFT开发的小技巧,让大家可以花几分钟学到一些好玩的东西

2016年7月26日

可变参数(Variadic Arguments)

在OC中最常用的函数之一应该就是NSLog这个输出函数了,作为有好奇心的新青年,我“手贱”的点进了NSLog的声明里看到了下面的声明:

FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);  

这个代码然我瞬间感觉好熟悉,想起了之前Java中的可变参数函数。可是再仔细一看就发现,尼玛后面的可变参数竟然是没有参数名,而且也没有类型之类的信息,瞬间我就懵逼了。

于是我就找了我的大哥--谷歌,经过一番查找之后发现其实也不是特别难。乍一看虽然跟Java中将参数默认识别为一个具体类型的数据完全不同,但是其实还是换汤不换药,其实只是我们将Java中帮我们做的那一份我们自己实现了而已。

其实说是OC的可变参数的实现不如说是C的可变参数的实现,因为OC是C的超集,而且在许多方面来说OC并没有完全的摆脱C的影子。好了不废话了,看招:

// 可变参数的实现
-(NSNumber *)sumAllValues:(NSNumber *)initValue, ... {
    va_list args; // 1.
    va_start(args, initValue); // 2.
    NSNumber *arg = va_arg(args, NSNumber *); // 3.
    NSNumber *result = initValue;
    float sum = [result floatValue];
    while (arg != nil) {
        sum += [arg floatValue];
        arg = va_arg(args, NSNumber *); // 4.
    }
    va_end(args); // 5.
    result = @(sum);
    return result;
}

可以看出其实我们只是将Java帮我们做的事情让我们自己做而已。也不是特别难嘛。 这里稍微给大家讲解一下:

  • 1.声明了一个变量用来装可变参数,其类型是在C中定义的va_list, 其本质上来说是一个指针
  • 2.调用C函数va_start, 其第一个参数是将可变参数的第一个参数的指针传递给该参数,第二个参数指明了可变参数是跟在哪个参数之后的
  • 3、4.都是用来去多参数的下一个参数的C函数,其第一个参数传递的是1中声明的变量,第二个参数是讲多参数的数据转换成什么类型,其实这个好像是forin的形式
  • 5.在遍历完可变参数之后记得要调用va_end来表明结束,并且释放该变量

其实这个过程很想是在写原生sqlite3的时候,获取数据之后进行遍历数据的情况。需要去某个地方(va_start第二个参数之后的位置)"获取"数据,然后遍历数据,最后结束的时候还要关闭“数据库”(释放变量)的感觉。是不是很简单?

那么接下来我们再看看Swift是如何实现的,更简单的就来了~

func sumAllValues(nums: Float...) -> Float {  
    var sum: Float = 0.0
    for num in nums {
        sum += num
    }
    return sum
}

sumAllValues(5.0, 4.0, 5.0, 3.3)  

有没有觉得很像是一个语法糖一样,只是将可变参数转换成一个数据,于是我又一次“手贱”(怪我咯?)的查看了一下nums的类型:

可变参数

看出来没,其实所谓的可变参数都是骗子。就不能少一点套路,多一点真诚吗?其实就是讲所谓的可变参数编程了一个Array了。所以总体来说Swift的可变参数函数实现起来就是讲可变参数变成了数组。

然而其实在OC中大部分情况下没必要用到可变参数函数, 我们可以用NSArray等集合工具来实现即可。并且用集合来实现相对更安全一点。毕竟用集合可以通过集合的许多属性和方法来做一些特殊的处理和判断。所以,切记可变函数参数在极少的情况下才会用到,大部分情况我们只要了解其中的实现以及看见的时候知道是怎么回事即可。


 

void StringUtil::OutputDebugString(LPCTSTR lpszFormat, ...)
{
    CString str;
    va_list valist;
    va_start(valist, lpszFormat);
    str.FormatV(lpszFormat, valist);
    va_end(valist);
    ::OutputDebugString(str);
}

 

#include <stdio.h>// 包含一个头文件,提供不定参数的宏
#include <stdarg.h>// 用于输出不定数量的整数值
void print(int count, ...)
{
    // 1. 使用 va_list 定义一个变量
    va_list valist;
​
    // 2. 需要使用 va_start 初始化
    va_start(valist, count);
​
    // 3. 从参数列表中获取所有的数据
    for (int i = 0; i < count; ++i)
    {
        // 接收 int 类型的数据
        int number = va_arg(valist, int);
        printf("%d ", number);
    }
​
    // 4. 需要使用 va_end 结束
    va_end(valist);
}
​
int main()
{
    print(3, 1, 2, 3);
    print(5, 1, 2, 3, 4, 5);
​
    return 0;
}

 


5.5 汇编语言:函数调用约定

 

函数是任何一门高级语言中必须要存在的,使用函数式编程可以让程序可读性更高,充分发挥了模块化设计思想的精髓,今天我将带大家一起来探索函数的实现机理,探索编译器到底是如何对函数这个关键字进行实现的,并使用汇编语言模拟实现函数编程中的参数传递调用规范等。

说到函数我们必须要提起调用约定这个名词,而调用约定离不开栈的支持,栈在内存中是一块特殊的存储空间,遵循先进后出原则,使用push与pop指令对栈空间执行数据压入和弹出操作。栈结构在内存中占用一段连续存储空间,通过esp与ebp这两个栈指针寄存器来保存当前栈起始地址与结束地址,每4个字节保存一个数据。

当栈顶指针esp小于栈底指针ebp时,就形成了栈帧,栈帧中可以寻址的数据有局部变量,函数返回地址,函数参数等。不同的两次函数调用,所形成的栈帧也不相同,当由一个函数进入另一个函数时,就会针对调用的函数开辟出其所需的栈空间,形成此函数的独有栈帧,而当调用结束时,则清除掉它所使用的栈空间,关闭栈帧,该过程通俗的讲叫做栈平衡。而如果栈在使用结束后没有恢复或过度恢复,则会造成栈的上溢或下溢,给程序带来致命错误。

一般情况下在Win32环境默认遵循的就是STDCALL,而在Win64环境下使用的则是FastCALL,在Linux系统上则遵循SystemV的约定,这里我整理了他们之间的异同点.

  • CDECL:C/C++默认的调用约定,调用方平栈,不定参数的函数可以使用,参数通过堆栈传递.
  • STDCALL:被调方平栈,不定参数的函数无法使用,参数默认全部通过堆栈传递.
  • FASTCALL32:被调方平栈,不定参数的函数无法使用,前两个参数放入(ECX, EDX),剩下的参数压栈保存.
  • FASTCALL64:被调方平栈,不定参数的函数无法使用,前四个参数放入(RCX, RDX, R8, R9),剩下的参数压栈保存.
  • System V:类Linux系统默认约定,前八个参数放入(RDI,RSI, RDX, RCX, R8, R9),剩下的参数压栈保存.

首先先来写一段非函数版的堆栈使用案例,案例中模拟了编译器如何生成Main函数栈帧以及如何对栈帧初始化和使用的流程,笔者通过自己的理解写出了Debug版本的一段仿写代码。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
  main PROC
    push ebp                   ; 保存栈底指针ebp
    mov ebp,esp                ; 调整当前栈底指针到栈顶
    
    sub esp,0e4h               ; 抬高栈顶esp开辟局部空间
    
    push ebx                   ; 保存寄存器
    push esi
    push edi
    
    lea edi,dword ptr [ ebp - 0e4h ]  ; 取出当前函数可用栈空间首地址
    
    mov ecx,39h                       ; 填充长度
    mov eax,0CCCCCCCCh                ; 填充四字节数据
    rep stosd                         ; 将当前函数局部空间填充初始值
    
    ; 使用当前函数可用局部空间
    xor eax,eax
    
    mov dword ptr [ ebp - 08h ],1
    mov dword ptr [ ebp - 014h ],2       ; 使用局部变量
    mov dword ptr [ ebp - 020h ],3
    
    mov eax,dword ptr [ ebp - 014h ]
    add eax,dword ptr [ ebp - 020h ]
    
    ; 如果指令影响了堆栈平衡,则需要平栈
    push 4                ; 此情况,由于入栈时没有修改过,平栈只需add esp,12
    push 5
    push 6                ; 如果代码没有自动平栈,则需要手动平
    add esp,12            ; 每个指令4字节 * 多少条影响
    
    push 10
    push 20
    push 30               ; 使用3条指令影响堆栈
    pop eax
    pop ebx               ; 弹出两条
    add esp,4             ; 修复堆栈时只需要平一个变量
    
    pop edi               ; 恢复寄存器
    pop esi
    pop ebx
    
    add esp,0e4h        ; 降低栈顶esp开辟的局部空间,局部空间被释放
    
    cmp ebp,esp         ; 检测堆栈是否平衡,不平衡则直接停机
    jne error
    
    pop ebp
    mov esp,ebp         ; 恢复基址指针
    int 3
  
  error:
    int 3
  
  main ENDP
END main

5.1 CDECL

CDECL是C/C++中的一种默认调用约定(调用者平栈)。这种调用方式规定函数调用者在将参数压入栈中后,再将控制权转移到被调用函数,被调用函数通过栈顶指针ESP来访问这些参数。函数返回时,由调用者程序负责将堆栈平衡清除。CDECL调用约定的特点是简单易用,但相比于其他调用约定,由于栈平衡的操作需要在函数返回后再进行,因此在一些情况下可能会带来一些性能上的开销。

该调用方式在函数内不进行任何平衡参数操作,而是在退出函数后对esp执行加4操作,从而实现栈平衡。该约定会采用复写传播优化,将每次参数平衡的操作进行归并,在函数结束后一次性平衡栈顶指针esp,且不定参数函数也可使用此约定。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
  function PROC
    push ebp
    mov ebp,esp
    sub esp,0cch
    push ebx
    push esi
    push edi
    
    lea edi,dword ptr [ ebp - 0cch ]     ; 初始化局部变量
    mov ecx,33h
    mov eax,0CCCCCCCCh
    rep stosd

    mov eax,dword ptr [ ebp + 08h ]       ; 第一个变量(传入参数1)
    add eax,dword ptr [ ebp + 0Ch ]       ; 第二个变量(传入参数2)
    add eax,dword ptr [ ebp + 10h ]       ; 第三个变量(传入参数3)
    
    mov dword ptr [ ebp - 08h ],eax       ; 将结果放入到局部变量
    mov eax,dword ptr [ ebp - 08h ]       ; 给eax寄存器返回
    
    pop edi
    pop esi
    pop ebx
    mov esp,ebp
    pop ebp
    ret
  function endp

  main PROC
    ; 单独调用并无优势
    push 3
    push 2
    push 1
    call function          ; __cdecl functin(1,2,3)
    add esp,12
    
    ; 连续调用则可体现出优势
    push 5
    push 4
    push 3
    call function          ; __cdecl function(3,4,5)
    mov ebx,eax
    
    push 6
    push 7
    push 8
    call function          ; __cdecl function(8,7,6)
    mov ecx,eax
    
    add esp,24             ; 一次性平两次栈
    
    int 3
  main ENDP
END main

5.2 STDCALL

STDCALL 调用约定规定由被调用者负责将堆栈平衡清除。STDCALL是一种被调用者平栈的约定,这意味着,在函数调用过程中,被调用函数使用栈来存储传递的参数,并在函数返回之前移除这些参数,这种方式可以使调用代码更短小简洁。STDCALL与CDECL只在参数平衡上有所不同,其余部分都一样,但该约定不定参数函数无法使用。

通过以上分析发现_cdecl_stdcall两者只在参数平衡上有所不同,其余部分都一样,但经过优化后_cdecl调用方式的函数在同一作用域内多次使用,会在效率上比_stdcall髙,这是因为_cdecl可以使用复写传播优化,而_stdcall的平栈都是在函数内部完成的,无法使用复写传播这种优化方式。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
  function PROC
    push ebp
    mov ebp,esp
    sub esp,0cch
    push ebx
    push esi
    push edi
    
    lea edi,dword ptr [ ebp - 0cch ]     ; 初始化局部变量
    mov ecx,33h
    mov eax,0CCCCCCCCh
    rep stosd

    mov eax,dword ptr [ ebp + 08h ]       ; 第一个变量(传入参数1)
    add eax,dword ptr [ ebp + 0Ch ]       ; 第二个变量(传入参数2)
    add eax,dword ptr [ ebp + 10h ]       ; 第三个变量(传入参数3)
    
    mov dword ptr [ ebp - 08h ],eax       ; 将结果放入到局部变量
    mov eax,dword ptr [ ebp - 08h ]       ; 给eax寄存器返回
    
    pop edi
    pop esi
    pop ebx
    mov esp,ebp
    pop ebp
    
    ret 12                                ; 应用stdcall时,通过ret对目标平栈
  function endp

  main PROC
    push 3
    push 2
    push 1
    call function          ; __stdcall functin(1,2,3)
    mov ebx,eax            ; 获取返回值
    
    push 4
    push 5
    push 6
    call function          ; __stdcall function(6,5,4)
    mov ecx,eax            ; 获取返回值
    
    add ebx,ecx            ; 结果相加
    int 3
  main ENDP
END main

5.3 FASTCALL

FASTCALL是一种针对寄存器的调用约定。它通常采用被调用者平衡堆栈的方式,类似于STDCALL调用约定。但是,FASTCALL约定规定函数的前两个参数在ECX和EDX寄存器中传递,节省了压入堆栈所需的指令。此外,函数使用堆栈来传递其他参数,并在返回之前使用类似于STDCALL约定的方式来平衡堆栈。

FASTCALL的优点是可以在发生大量参数传递时加快函数的处理速度,因为使用寄存器传递参数比使用堆栈传递参数更快。但是,由于FASTCALL约定使用的寄存器数量比CDECL和STDCALL约定多,因此它也有一些限制,例如不支持使用浮点数等实现中需要使用多个寄存器的数据类型。

FASTCALL效率最高,其他两种调用方式都是通过栈传递参数,唯独_fastcall可以利用寄存器传递参数,一般前两个或前四个参数用寄存器传递,其余参数传递则转换为栈传递,此约定不定参数函数无法使用。

  • 对于32位来说使用ecx,edx传递前两个参数,后面的用堆栈传递。
  • 对于64位则会使用RCX,RDX,R8,R9传递前四个参数,后面的用堆栈传递。
  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
  function PROC
    push ebp
    mov ebp,esp
    sub esp,0e4h
    push ebx
    push esi
    push edi
    push ecx
    
    lea edi,dword ptr [ ebp - 0e4h ]     ; 初始化局部变量
    mov ecx,39h
    mov eax,0CCCCCCCCh
    rep stosd
    pop ecx
    
    mov dword ptr [ ebp - 14h ],edx      ; 读入第二个参数放入局部变量
    mov dword ptr [ ebp - 8h ],ecx       ; 读入第一个参数放入局部变量
    
    mov eax,dword ptr [ ebp - 8h ]       ; 从局部变量内读入第一个参数
    add eax,dword ptr [ ebp - 14h ]      ; 从局部变量内读入第二个参数
    
    add eax,dword ptr [ ebp + 8h ]       ; 从堆栈中读入第三个参数
    add eax,dword ptr [ ebp + 0ch ]      ; 从堆栈中读入第四个参数
    add eax,dword ptr [ ebp + 10h ]      ; 从堆栈中读入第五个参数
    add eax,dword ptr [ ebp + 14h ]      ; 从堆栈中读入第六个参数
    
    mov dword ptr [ ebp - 20h ],eax      ; 将结果给第三个局部变量
    mov eax,dword ptr [ ebp - 20h ]      ; 返回数据
    
    pop edi
    pop esi
    pop ebx
    mov esp,ebp
    pop ebp
    
    ret 16                               ; 平栈
  function endp

  main PROC
    push 6
    push 5
    push 4
    push 3
    mov edx,2
    mov ecx,1         ; __fastcall function(1,2,3,4,5,6)
    call function     ; 调用函数
    
    int 3
  main ENDP
END main

5.4 使用ESP寄存器寻址

编译器开启了O2优化模式选项,则为了提高程序执行效率,只要栈顶是稳定的,编译器编译时就不再使用ebp指针了,而是利用esp指针直接访问局部变量,这样可节省一个寄存器资源。

在程序编译时编译器会自动为我们计算ESP基地址与传入变量的参数偏移,使用esp寻址后,不必每次进入函数后都调整栈底ebp,从而减少了ebp的使用,因此可以有效提升程序执行效率。但如果在函数执行过程中esp发生了变化,再次访问变量就需要重新计算偏移了。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
  function PROC
    push ebp
    mov ebp,esp
    sub esp,0ch
    push esi
    
    ; 动态计算出四个参数
    lea eax,dword ptr [ esp - 4h + 01ch ]    ; 计算参数1 [esp+18]
    lea ebx,dword ptr [ esp - 0h + 01ch ]    ; 计算参数2 [esp+1c]
    lea ecx,dword ptr [ esp + 4h + 01ch ]    ; 计算参数3 [esp+20]
    lea edx,dword ptr [ esp + 8h + 01ch ]    ; 计算参数4 [esp+24]
    
    ; 如果ESP被干扰则需要动态调整
    lea eax,dword ptr [ esp - 4h + 01ch ]          ; 当前参数1的地址
    push ebx
    push ecx                                       ; 指令让ESP被减去8
    lea eax,dword ptr [ esp - 4h + 01ch  + 8h ]    ; 此处需要+8h修正堆栈
    
    add esp,0ch
    pop esi
    mov esp,ebp
    pop ebp
    ret
  function endp

  main PROC
    push 5
    push 3
    push 4
    push 1
    call function
    int 3
  main ENDP
END main

5.5 使用数组指针传值

这里我们以一维数组为例,二维数组的传递其实和一维数组是相通的,只不过在寻址方式上要使用二维数组的寻址公式,此外传递数组其实本质上就是传递指针,所以数组与指针的传递方式也是相通的。

使用汇编仿写数组传递方式,在main函数内我们动态开辟一块栈空间,并将数组元素依次排列在栈内,参数传递时通过lea eax,dword ptr [ ebp - 18h ]获取到数组栈地址空间,由于main函数并不会被释放所以它的栈也是稳定的,调用function函数时只需要将栈首地址通过push eax的方式传递给function函数内,并在函数内通过mov ecx,dword ptr [ ebp + 8h ]获取到函数基地址,通过比例因子定位栈空间。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
  function PROC
    push ebp
    mov ebp,esp
    sub esp,0cch
    push ebx
    push esi
    push edi
    lea edi,dword ptr [ ebp - 0cch ]
    mov ecx,33h
    mov eax,0CCCCCCCCh
    rep stosd
    
    ; 检索数组第一个元素
    mov eax,1
    mov ecx,dword ptr [ ebp + 8h ]          ; 定位数组基地址
    mov edx,dword ptr [ ecx + eax * 4 ]     ; 定位元素
    
    ; 检索数组第二个元素
    mov eax,2
    mov ecx,dword ptr [ ebp + 8h ]
    mov edx,dword ptr [ ecx + eax * 4 ]
    
    pop edi
    pop esi
    pop ebx
    add esp,0cch
    mov esp,ebp
    pop ebp
    ret
  function ENDP

  main PROC
    push ebp
    mov ebp,esp
    sub esp,0dch
    push ebx
    push esi
    push edi

    lea edi,dword ptr [ ebp - 0dch ]
    mov ecx,37h
    mov eax,0CCCCCCCCh
    rep stosd 
    
    mov dword ptr [ ebp - 18h ],1        ; 局部空间存储数组元素
    mov dword ptr [ ebp - 14h ],2
    mov dword ptr [ ebp - 10h ],3
    mov dword ptr [ ebp - 0ch ],4
    mov dword ptr [ ebp - 8h ],5
    
    push 5
    lea eax,dword ptr [ ebp - 18h ]      ; 取数组首地址并入栈
    push eax
    call function                        ; 调用函数 function(5,eax)
    add esp,8                            ; 平栈

    pop edi
    pop esi
    pop ebx
    add esp,0dch
    mov esp,ebp
    pop ebp
    ret
  main ENDP
END main

5.6 指向函数的指针

程序通过CALL指令跳转到函数首地址执行代码,既然是地址那就可以使用指针变量来存储函数的首地址,该指针变量被称作函数指针。

在编译时编译器为函数代码分配一段存储空间,这段存储空间的起始地址就是这个函数的指针,我们可以调用这个指针实现间接调用指针所指向的函数。

#include <iostream>

void __stdcall Show(int x, int y)
{
  printf("%d --> %d \n",x,y);
}

int __stdcall ShowPrint(int nShow, int nCount)
{
  int ref = nShow + nCount;
  return ref;
}

int main(int argc, char* argv[])
{
  // 空返回值调用
  void(__stdcall *pShow)(int,int) = Show;
  pShow(1,2);

  // 带参数调用返回
  int(__stdcall *pShowPrint)(int, int) = ShowPrint;
  int Ret = pShowPrint(2, 4);
  printf("返回值 = %d \n", Ret);

  return 0;
}

首先我们使用汇编仿写ShowPrint函数以及该函数所对应的int(__stdcall *pShowPrint)(int, int)函数指针,看一下在汇编层面该如何实现这个功能。

  .386p
  .model flat,stdcall
  option casemap:none

include windows.inc
include kernel32.inc
includelib kernel32.lib

.code
  function PROC
    push ebp
    mov ebp,esp
    sub esp,0cch
    push ebx
    push esi
    push edi
    lea edi,dword ptr [ ebp - 0cch ]
    mov ecx,33h
    mov eax,0CCCCCCCCh
    rep stosd
    
    mov eax,dword ptr [ ebp + 4h ]    ; 此处+4得到的是返回后上一条指令地址
    mov eax,dword ptr [ ebp + 8h ]    ; 得到第一个堆栈传入参数地址
    mov ebx,dword ptr [ ebp + 0ch ]   ; 得到第二个堆栈传入参数地址
    add eax,ebx                       ; 递增并返回到EAX
    
    pop edi
    pop esi
    pop ebx
    add esp,0cch
    mov esp,ebp
    pop ebp
    ret
  function ENDP

  main PROC
    push ebp
    mov ebp,esp
    sub esp,0d8h
    push ebx
    push esi
    push edi

    lea edi,dword ptr [ ebp - 0d8h ]
    mov ecx,36h
    mov eax,0CCCCCCCCh
    rep stosd 
    
    lea eax,function                     ; 获取函数指针
    mov dword ptr [ ebp - 8h ],eax       ; 将指针放入局部空间
    
    push 4
    push 2                               ; 传入参数
    call dword ptr [ ebp - 8h ]          ; 调用函数
    add esp,8                            ; 平栈

    pop edi
    pop esi
    pop ebx
    add esp,0d8h
    mov esp,ebp
    pop ebp
    ret
  main ENDP
END main

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/17fb1a42.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

posted on 2010-03-18 10:14  不及格的程序员-八神  阅读(1193)  评论(1编辑  收藏  举报