MSVC平台下X64处理器函数调用规则——底层机制

目录

1. 默认的调用规则

2. 内存边界对齐问题

3. 解开性(Unwindability)

4. 参数传递

5. 可变数量参数的传递

6. 非原型函数

7. 函数返回值

8. 调用函数或被调函数存储寄存器

9. 函数指针

10. 对较旧代码的浮点支持

11. 浮点状态和控制寄存器(Float Point Status and Control Register,简记为FPSCR)

12. 多媒体扩展控制和状态寄存器(Multimedia Extensions Control and Status Register,简记为MXCSR,注:因E在计算机单词缩写中已留作他用,因此E开始的单词,缩写时往往采用单词第2个字母大写)

13. 跳转指令(setjmp,longjmp)


这里介绍的是在X86代码中,一个函数(调用函数(caller))对另一个函数(被调函数(callee))调用的标准处理方式和有关约定。

1. 默认的调用规则

按照默认规则,X64应用二制接口(Application Binary Interface,简记为ABI)采用一个四寄存器快速调用(a four-register fast-call)的调用规则。系统在调用栈分配一块影子内存(shadow)来存储这四个寄存器的值。

在函数传递的参数与四个寄存器之间,规定了严格的一一对应关系。任何不适合按8字节,或者按1,2,4,或8字节传递的参数,必须通过引用方式进行传递(即,通过内存栈进行传递)。单个参数永远不会跨越多个寄存器进行传递。

X87浮点寄存器栈(指的是寄存器和调用中使用到的内存栈,是两个不同的东西,统称“寄存器栈”)没有使用。可以被被调函数使用,但应考虑到它在函数调用的过程中是易失性的(即,用完不用管恢复其值)。所有的浮点操作都是通过这16个XMM寄存器完成。整型参数通过将参数放在RCX,RDX,R8和R9这四个寄存器中进行传递。浮点参数通过将参数放在XMM0L, XMM1L, XMM2L,和XMM3L这四个寄存器中进行传递。参数传递的详细过程稍后介绍。这些用于参数传递的寄存器,和RAX, R10, R11, XMM4, 和XMM5寄存器一起,统称为易失性寄存器(volatile)(即,使用后不管恢复其值,也不会造成任何影响)。寄存器用法,在参见文档的寄存器用法页(register usage)和调用/被调用存储寄存器页(caller/calleesavedregister)。

对于原型函数(prototyped function,即,显式定义了参数),所有参数在被传递前,都会传化成符合被调用函数参数的类型。调用函数负责为被调函数的参数分配存储空间。调用函数必须为被调函数分配存储四个寄存器参数的足够容量的内存空间,即使被调函数用不了四个参数(即,不管被调函数有几个参数,那怕是0个参数,也需要分配这些内存空间,正所谓“影子内存”,这一点对编程很重要)。这个调用规则使得同时支持原型C语言函数和可变参数的C/C++函数得到简化。对于可变参数函数或非原型函数,任何浮点值必须在相应的通用寄存器中复制。超过四个参数的任何参数在调用之前必须将其存储在影子内存之后的内存栈中以便进行传递(即,从影子内存之后开始存储,不能硬破影响内存部分;编程时程序员需要处理这一点)。可变参数函数细节参见可变参数函数文档部分,非原型函数信息细节参见非原型函数文档部分。

2. 内存边界对齐问题

大部分结构在它们的自然边界对齐。主要的例外是栈指针和利用mallocalloca动态分配的堆内存,为便于性能提升,它们都按16字节的内存地址边界对齐(即,内存地址的边界能被16整除)。16字节以上的边界对齐必须手动处理。因为,对于XMM寄存器的操作而言,16字节是一个通用的对齐大小,这个值对于大部分的代码都有效。更多关于结构布局和对齐的信息,参见类型和存储部分的文档。关于栈分布的信息,参见X64栈用法部分的文档。

3. 解开性(Unwindability)

叶函数(leaf functions)是一类不会改变任何非易失性寄存器值的函数。而一个非叶函数(anon-leaf function)可以改变非易失性寄存器的值,例如,通过调用函数,修改RSP寄存器的值。或者,通过为局部变量分配额外的栈空间,它也会改变RSP寄存器的值。为了在处理异常时恢复非易失性寄存器的值,非叶函数使用静态数据进行标注(annotated)。静态数据说明了如何使用任意指令恰当地解开函数。静态数据存储为“pdata”、或者存储为例程数据(procedure data),这反过来又指的是“xdata”,即异常处理数据。

序言(prologs)和结语(epilogs)受到高度限制,因此可以在xdata中正确地描述它们(指在xdata中描述序言和尾声)。除了叶函数以外,栈指针在不是序言和结语代码块的任何代码区域中必须保持16字节边界对齐。叶函数可以通过简单地模拟一个返回的方式解开函数,因此,不要求使用pdata和xdata,关于函数序言和结语的恰当结构,请参见X64序言和结语这部分文档。关于异常处理,以及异常处理和利用pdata和xdata解开函数问题,参见X64异常处理这部分文档。

4. 参数传递

按照默认约定,函数的前四个参数通过寄存器传递。用寄存器传递参数取决于参数的位置和类型。超过四个的函数参数按从右向左的顺序压入调用函数的内存栈中进行传递。

最左边的四个整型参数按从左到右的顺序分别存入RCX,RDX,R8和R9这四个寄存器中。第5个及以后的参数通过前述的内存栈进行传递。寄存器中的所有整数参数都是右对齐的,因此被调函数可以忽略寄存器的高位并仅访问寄存器的必要部分。任何前四个单精度和双精度类型参数都是通过从XMM0到XMM3这四个寄存器进行传递,具体取决于其位置。当它们是可变参数的时候,单精度浮点数只是放在RCX,RDX,R8和R9这四个寄存器中。具体细节参见可变参数的文档。同样地,当对应的参数是一个整型(整数或指针类型)时,系统忽略XMM0到XMM3这四个寄存器。

__m128类型(__m128 数据类型用于流式 SIMD 扩展和流式 SIMD 扩展-2指令内在函数,定义在<xmmintrin.h>中),数组和字符串从不使用立即值传递。代之的是,一个指针被传递给调用函数分配的内存(即,调用者分配栈内存存放的是数组或字符串的内存指针,这个指针存储的是数组或字符串在内存中的位置)。大小为 8、16、32 或 64 位以及 __m64 类型的结构和联合,当成相同大小的整数传递。其它大小的结构或联合通过调用函数分配的内存传递(即栈内存存放参数的指针)。对于这些作为指针传递的聚合类型(Aggregates types),包括__m128类型,调用者分配的临时性内存必须按16字节边界对齐。

内置函数不分配栈空间,并且不调用其它函数,有时候,它们会使用其它的易失性寄存器传递额外的寄存器参数。通过编译器与内置函数之间的紧绑定来实现这种优化。

如果有需要,被调函数负责将寄存器参数转储到它们的影子存储空间中。

下面的例表总结了参数传递过程,按类型和位置从左到右:

参数类型

第5个及以上参数

第4个

第3个

第2个

第1个

单精度浮点数

栈(指调用函数分配的栈,下同)

XMM3

XMM2

XMM1

XMM0

整型(整数和指针)

R9

R8

RDX

RCX

聚合类型(8、16、32 或 64 位)和 __m64

R9

R8

RDX

RCX

其它聚合类型,例如指针

R9

R8

RDX

RCX

_m128,例如指针

R9

R8

RDX

RCX

以下均以C++函数为例展示函数参数传递过程。

示例1:所有参数为有符号整数的参数传递:

func1(int a, int b, int c, int d, int e, int f);

// a 放入RCX, b 放入 RDX, c 放入 R8, d 放入 R9, f e 按先后顺序压入栈中传递

示例2:所有参数为浮点数的参数传递(单精度,又精度):

func2(float a, double b, float c, double d, float e, float f);

// a 放入XMM0, b 放入 XMM1, c 放入 XMM2, d 放入 XMM3, f e 按先后顺序压入栈中传递

示例3:混合有有符号整型和浮点型的参数传递(单精度,又精度):

func3(int a, double b, int c, float d, int e, float f);

// a 放入RCX, b 放入 XMM1, c 放入 R8, d 放入 XMM3, f e 按先后顺序压入栈中传递

(注意到,以上例子中,按从左向右的顺序,而不管是浮点型还是整型。)

示例4:__m64 、__m128和聚合类型参数传递:

func4(__m64 a, __m128 b, struct c, float d, __m128 e, __m128 f);

a 放入RCX, b 放入 RDX, 指向c的指针放入 R8, d 放入 XMM3, 指向f e 的指针按先后顺序压入栈中传递

5. 可变数量参数的传递

    假如参数通过可变参数数量方式传递(例如,省略号形式的函数参数),则按照规范参数传递约定执行。包括超过五个及以上参数采用栈空间传递的约定。被调函数负责转储已取得地址的参数。仅对于浮点参数,整型寄存器和浮点寄存器都必须包含该值,以防被调函数期望从整数寄存器取得该值(即,可变参数方式时,仅对于浮点参数,对应浮点寄存器和整型寄存器都必须存储这个浮点值,以便于某些计算)。

6. 非原型函数

    对于未完全原型化(Unprototyped functions,即,没有显式定义参数)的函数,调用函数以整型传递整型值,以双精度传递浮点值。仅对于浮点参数,整型寄存器和浮点寄存器都必须包含该值,以防被调函数期望从整数寄存器取得该值(即,可变参数方式时,仅对于浮点参数,对应浮点寄存器和整型寄存器都必须存储这个浮点值,以便于某些计算)。

例如,以下C++代码:

func1();

func2() {   // RCX = 2, RDX = XMM1 = 1.0, 以及R8 = 7

   func1(2, 1.0, 7);

}

7. 函数返回值

    标量(scalar)返回值适用于64位(包括__m64类型),通过放在RAX寄存器中返回。非标量(Non-scalar)返回值(包括单精度、双精度和向量类型,例如__m128、__m128i、__m128d)通过放在XMM0中返回。RAX或XMM0 返回的值中未使用位的状态未定义。

    用户定义类型可以通过全局函数和静态成员函数的值返回。要使用RAX中的值返回用户定义数据,要求数据必须具有1、2、4、8、16、32或64位的长度。同时,也必须满足没有用户定义的构造函数,析构函数,或者拷贝赋值运算符。它不能有私有或保护类非静态数据成员,也不能有非静态的引用类型数据成员。不能有基类或虚函数。以及,只能有满足这些要求的数据成员。(这种定义在本质上与 C++03 POD 类型一样。因为这种定义在C++11标准中已经改变,我们不推荐使用std::is_pod来测试这个定义。) 不然,调用函数必须为返回类型分配内存,并将指向它的指针作为第一个参数传递。然后将剩余的参数向右移动一个参数,同一个指针必须由被调函数放在RAX中返回。

    下面这些示例展示了如何为具有指定声明的函数传递参数和返回值(C++函数):

返回值示例1——64位结果

__int64 func1(int a, float b, int c, int d, int e);

// 调用函数将a放入RCX, b 放入 XMM1, c 放入 R8, d 放入 R9, e压入函数栈,

// 被调函数用RAX返回__int64的结果。

返回值示例2——128位结果

__m128 func2(float a, double b, int c, __m64 d);

// 调用函数将a放入XMM0, b 放入 XMM1, c 放入 R8, d 放入 R9,

// 被调函数用XMM0返回__m128的结果。

返回值示例3——通过指针返回用户定义类型的结果

struct Struct1 {

   int j, k, l;    // Struct1 exceeds 64 bits.

};

Struct1 func3(int a, double b, int c, float d);

//调用函数为用户定义的返回类型Struct1分配内存,并通过RCX传递分配内存的指针,

// a放入RDX,b放入XMM2,c放入R9,d压入函数栈,

// 被调函数用RAX返回指向结构Struct1的指针结果。

返回值示例4——通过值返回用户定义类型的结果

struct Struct2 {

   int j, k;    // Struct2 适合64, 满足按值返回的要求。

};

Struct2 func4(int a, double b, int c, float d);

// 调用函数将 a 放入RCX, b 放入 XMM1, c 放入 R8, 以及将d 放入XMM3;

// 被调函数按值以RAX返回Struct2类型的结果。

8. 调用函数或被调函数存储寄存器

X64 ABI认为寄存器 RAX、RCX、RDX、R8、R9、R10、R11 和 XMM0-XMM5易失性的。当存在时(指后面这些寄存器存在时),YMM0-YMM15 和 ZMM0-ZMM15 的上部分也是易失性的。 在 AVX512VL 上,ZMM、YMM 和 XMM 寄存器 16-31 也是易失性的。 应考虑易失性寄存器的值在函数调用时被销毁,除非通过分析(例如整个程序优化)可以证明安全(指利用易失性寄存器的值安全)。

X64 ABI认为寄存器RBX、RBP、RDI、RSI、RSP、R12、R13、R14、R15 和 XMM6-XMM15是非易失性寄存器。它们必须由使用它们的函数负责存储和恢复(即,使用前要将其值保存起来,用完后使用保存的值去恢复其原来的值,否则可以导致灾难后果)。

9. 函数指针

函数指针只是指向相应函数标签的指针。函数指针没有目录 (Table Of Contents,简记为TOC)要求。

10. 对较旧代码的浮点支持

MMX和浮点栈寄存器(MM0-MM7/ST0-ST7)预留作上下文切换。对这些寄存器的访问没有显式的调用规则。在内核模式的代码中,严格禁用这些寄存器。

11. 浮点状态和控制寄存器(Float Point Status and Control Register,简记为FPSCR)

寄存器状态也包括X87浮点处理单元控制字(Float Process Unit,简记为FPU;控制字(Control Word))。调用约定表明这个寄存器是非易失性的。X87浮点处理单元控制字在程序执行的开始通过下面的标准值获得设置:

寄存(位)

设置

FPCSR[0:6]

异常掩码7个位全部置1(屏蔽所有异常)

FPCSR[7]

保留,置0

FPCSR[8:9]

精度控制-二进制10(双精度)

FPCSR[10:11]

舍入控制,0——舍入到最接近的值

FPCSR[12]

无限性控制(Infinity control),未使用,置0

被调函数如果修改了FPSCR寄存器内的任何域,在它返回调用函数之前,都必须恢复它们以前的值(非易失性寄存器)。另外,如果调用函数修改了这些域中的任何域的值,在它调用别的函数之前,都必须将其修改的域恢复成标准值,除非经过协商,修改值是新的被调函数所期望的值。

关于控制标识的非易失性规则,存在两种例外:

(1) 在这类函数中——给定函数的记录目的是为了修改非易失性寄存器FPSCR的标志(即,这类函数的存在目的就是为了修改FPSCR的标志)。

(2) 当它可以正确地证明,一个程序违背这些规则导致的运行结果与一个没有违背这些规则的程序运行结果一样,例如,通过整个程序的分析。

12. 多媒体扩展控制和状态寄存器(Multimedia Extensions Control and Status Register,简记为MXCSR,注:因E在计算机单词缩写中已留作他用,因此E开始的单词,缩写时往往采用单词第2个字母大写)

寄存器的状态也包括MXCSR。调用规则将寄存器分为易失性和非易失性两部分。易失性部分包括6个状态标识位,即MXCSR[0:5],而寄存器的其它部分,即MXCSR[6:15],被认为是非易失性的。

非易失性部分在程序开始执行时设置成下面的标准值:

寄存(位)

设置

MXCSR[6]

非正式是0,默认置0

MXCSR [7:12]

扩展掩码全部置1(屏蔽所有异常)

MXCSR [13:14]

舍入控制,0——舍得到最近的值

MXCSR [15]

刷新为0屏蔽下溢,0——关闭

被调函数如果修改了MXCSR寄存器内的任何易失性域,在它返回调用函数之前,都必须恢复它们以前的值(非易失性寄存器域)。另外,如果调用函数修改了这些域中的任何域的值,在它调用别的函数之前,都必须将其修改的域恢复成标准值,除非经过协商,修改值是新的被调函数所期望的值。

关于控制标识的非易失性规则,存在两种例外:

(1) 在这类函数中——给定函数的记录目的是为了修改寄存大MXCSR的非易失性域的标志(即,这类函数的存在目的就是为了修改MXCSR的非易失性域的标志)。

(2) 当它可以正确地证明,一个程序违背这些规则导致的运行结果与一个没有违背这些规则的程序运行结果一样,例如,通过整个程序的分析。

若非函数的文档明确说明,不要假设MXCSR寄存器在函数边界上的易失性部分状态。

13. 跳转指令(setjmp,longjmp)

当你在代码中包含了setjmpex.h或setjmp.h头文件,所有对setjmp或longjmp的调用都会导致一个解开函数的动作(unwind),它会触发系统调用析构函数和__finally标识的代码块。这个功能有别于X86调用规则,在X86环境下,包含了setjmp.h头文件会导致系统不调用__finally子句和析构函数。

对 setjmp 的调用会保留当前栈指针、非易失性寄存器和 MXCSR寄存器。对 longjmp 的调用返回到最近的 setjmp 调用站点,并将栈指针、非易失性寄存器和 MXCSR 寄存器重置为最近的 setjmp 调用所保留的状态。

内容来源:

x64 calling convention | Microsoft DocsDetails of the default x64 calling convention.https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170

模块原理: wow64 是在 64 位操作系统上允许 32 位程序(比如易编译的程序)执行的模拟器子系统;在 64 位操作系统中,不管你的程序是 32 还是 64 位的,其实都存在两个地址空间,正常情况下 32 位程序访问的自然是 32 位的地址空间,而 64 位程序访问其 64 位地址空间。 但是这两个空间是同时存在且可以切换的,本模块就是通过该原理切换到 64 位地址空间获取 ntdll.dll 相关函数进行调用(注:此基址是 64 位的,与平常获取的 32 位模块基址截然不同); 也就是:wow 环境 -> 进入 x64 环境 -> x64 函数调用x64 汇编代码 -> 退出 x64 环境 -> wow 环境,以上必须在一个子程序内完成; 部分实现代码借鉴 c++ 开源代码:wow64ext,在此感谢作者 rewolf。 模块功能: 实现易语言 纯 64 位汇编置入代码; 允许调用易程序 64 位 ntdll.dll 的所有函数,也就是你虽然开发的是 32 位程序,但可以实现很多 64 位函数所能实现的功能; 直接使用 64 位函数自由读写(注入) 64 位进程,与很多模块调用 NtWow64xxx 系列函数实现的方式有本质不同; 部分常用 ntdll.dll 函数已在模块直接提供,或以模拟 kernel32 函数的调用形式提供,v1.1 新增多个函数; 未提供函数获取地址后,可使用 X64Call 这个通用函数调用即可; 大部分提供的 64 位功能也同时提供了 32 位版本,以便兼容不同需求(模块在 32 位系统中不会开启 64 位功能引起异常,但 32 位功能依然可用); 支持加载任意 32/64 位 DLL,从此易语言可以调用外部 64 位 DLL 了(包括加载 kernel32.dll),v1.1 新增功能; 除了动态加载外,还支持 32 位 DLL 的内存加载,但 64 位只能加载本地 DLL 文件,v1.1 新增功能; 如有 BUG,请提供错误重现代码及执行环境,如非不可抗因素我都会及时更新的; 模块部分命令简述: 以下只是适用于 64 位的部分函数,模块中以相同命令形式实现的 32 位命令,这里就不列举了; 辅Zhu函数 fn_WOW64Enabled 如果你在代码中需要使用 64 位汇编或者操作 64 位进程,则初始化时应确保本函数返回真。实际只要是 64 位操作系统,均应返回 真 fn_ProcessIsX64 检测指定进程是否为 64 位进程 fn_CalcModOrFuncHash 使用过动态调用DLL的都清楚取模块基址和函数指针,微软默认使用字符串对比,本模块可使用哈希对比效率和易用上相对提升,本函数用于计算模块或函数哈希 易内部命令 X64Call 调用 64 位函数通用版本 X64CallArr 调用 64 位函数通用版本,数组方式传参,支持无限个数参数;【v1.2新增】 X64MemCopy 同类还有 X64MemCmp 函数;从 64 地址复制数据或 64 位地址与 32 位地址数据对比,但仅限进程内部 X64GetLong64 获取 64 位地址数值,同类函数还有:X64GetLong32、X64GetWord、X64GetByte X64GetTEB 取当前易程序 64 位 TEB,通过 TEB 再取 PEB,则进程和线程信息以及模块等一览无余了 GetNtdll64 ntdll.dll 在 64 位环境下的内存基址 GetModuleHandleEx64 通过模块哈希值获取其 64 位地址空间的内存基址(易进程而不是外部进程哦);同类还有 GetModuleHandle64 GetProcAddressEx64 通过函数哈希值或函数索引序号获取其 64 位调用地址;同类还有 GetProcAddress64 NtQuerySystemInformation64 cha询系统信息,可获取很多类别信息。这个 API 微软已不推荐使用并给出部分替代 API,但其个别功能十分好用且没用可替代品。cha询系统进程也是最全面的 OpenProcess64 打开进程句柄,关闭进程句柄时使用 CloseHandle64;【v1.1新增】 HeapAlloc64 堆管理函数,同类还有 GetDefaultHeap64/HeapReAlloc64/HeapFree64/HeapSize64;【v1.1新增】 malloc64 简化版默认堆管理函数,同类还有 realloc64/free64 RtlUnicodeToAnsi64 内核实现的 Unicode、Ansi 结构(不是数据指针)管理函数,同类还有:RtlInitAnsiString64/RtlFreeAnsiString64、RtlIni
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值