XSLT存档  

不及格的程序员-八神

 查看分类:  ASP.NET XML/XSLT JavaScripT   我的MSN空间Blog

x64 调用约定

本部分介绍 x64 代码中一个函数(调用方)调用另一个函数(被调用方)的标准流程和约定。

调用约定默认值
默认情况下,x64 应用程序二进制接口 (ABI) 使用四寄存器 fast-call 调用约定。 系统在调用堆栈上分配空间作为影子存储,供被调用方保存这些寄存器。

函数调用的参数与用于这些参数的寄存器之间有着严格的一一对应关系。 任何无法放入 8 字节或者不是 1、2、4 或 8 字节的参数都必须按引用传递。 单个参数永远不会分布在多个寄存器中。

未使用 x87 寄存器堆栈。 被调用方可能会使用它,但请考虑到它跨函数调用的易失性。 所有浮点数运算都使用 16 个 XMM 寄存器完成。

整数参数在寄存器 RCX、RDX、R8 和 R9 中传递。 浮点数参数在 XMM0L、XMM1L、XMM2L 和 XMM3L 中传递。 16 字节参数按引用传递。 要了解参数传递,请参阅参数传递一文。 这些寄存器和 RAX、R10、R11、XMM4 和 XMM5 被视为易失,或者可能在返回时由被调用方更改。 要详细了解寄存器的使用方法,请参阅 x64 寄存器使用和由调用方/被调用方保存的寄存器。

对于原型函数,在传递参数之前,所有参数都将转换为所需的被调用方类型。 调用方负责为被调用方的参数分配空间。 调用方必须始终分配足够的空间来存储 4 个寄存器参数,即使被调用方不使用这么多参数。 此约定简化了对非原型 C 语言函数和 vararg C/C++ 函数的支持。 对于 vararg 或非原型函数,任何浮点值都必须在相应的通用寄存器中重复。 调用之前,必须将除前 4 个参数外的其他参数存储在影子存储后面的堆栈中。 要详细了解 Vararg 函数,可参阅 Vararg。 要了解非原型函数,请参阅非原型函数一文。

对齐方式
大多数结构都按其自然对齐方式对齐。 主要的例外是堆栈指针和 malloc 或 alloca 内存;为了提高性能,它们对齐到 16 字节。 若要对齐到 16 字节以上,则必须手动完成。 由于 16 字节是 XMM 运算的常见对齐大小,因此该值应当适用于大多数代码。 有关结构布局和对齐方式的详细信息,请参阅 x64 类型和存储布局。 要了解堆栈布局,请参阅 x64 堆栈使用。

展开能力
叶函数是不更改任何非易失性寄存器的函数。 非叶函数可以通过调用函数来更改非易失性 RSP。 或者,它可以通过为局部变量分配额外的堆栈空间来更改 RSP。 若要在处理异常时恢复非易失性寄存器,非叶函数使用静态数据进行注释。 数据描述如何在任意指令下正确展开函数。 此数据存储为 pdata(过程数据),后者又引用 xdata(异常处理数据) 。 xdata 包含展开信息,并且可以指向其他 pdata 或异常处理程序函数。

Prolog 和 epilog 受到严格限制,因此可以在 xdata 中对其进行正确描述。 堆栈指针必须在任何不属于 epilog 或 prolog 的代码区域中保持 16 字节对齐,但在叶函数中除外。 只需模拟返回即可展开叶函数,因此 pdata 和 xdata 不是必需的。 要详细了解函数 prolog 和 epilog 的正确结构,请参阅 x64 prolog 和 epilog。 要详细了解异常处理以及 pdata 和 xdata 的异常处理和展开,请参阅 x64 异常处理。

参数传递
默认情况下,x64 调用约定将前 4 个参数传递给寄存器中的函数。 用于这些参数的寄存器取决于参数的位置和类型。 剩余的参数按从右到左的顺序推送到堆栈上。

最左边 4 个位置的整数值参数从左到右分别在 RCX、RDX、R8 和 R9 中传递。 如前所述,第 5 个和更高位置的参数在堆栈上传递。 寄存器中的所有整型参数都是向右对齐的,因此被调用方可忽略寄存器的高位,只访问所需的寄存器部分。

前四个参数中的所有浮点和双精度参数都在 XMM0 - XMM3(具体视位置而定)中传递。 存在 varargs 参数时,浮点值只放在整数寄存器 RCX、RDX、R8 和 R9 中。 有关详细信息,请参阅 Vararg。 同样,当相应的参数为整数或指针类型时,将忽略 XMM0 - XMM3 寄存器。

__m128 类型、数组和字符串从不通过即时值传递。 而是将指针传递给调用方分配的内存。 大小为 8、16、32 或 64 位的结构和联合以及 __m64 类型作为相同大小的整数传递。 其他大小的结构或联合作为指针传递给调用方分配的内存。 对于这些作为指针传递的聚合类型(包括 __m128),调用方分配的临时内存必须对齐 16 字节。

不分配堆栈空间且不调用其他函数的内部函数,有时使用其他易失性寄存器来传递其他寄存器参数。 编译器与内部函数实现之间的紧密绑定使此优化成为可能。

如果需要,被调用方负责将寄存器参数转储到其影子空间中。

下表总结了如何从左侧按类型和位置传递参数:

参数类型 第 5 个和更高位置 第 4 个 第3 个 第 2 个 最左侧
浮点 堆栈 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

 

x64 寄存器使用情况

x64 体系结构提供了 16 个通用寄存器(以后称为整数寄存器),以及 16 个可供浮点使用的 XMM/YMM 寄存器。 易失寄存器是由调用方假想的临时寄存器,并要在调用过程中销毁。 非易失寄存器需要在整个函数调用过程中保留其值,并且一旦使用,则必须由被调用方保存。

寄存器的易失性和保存方式

下表说明了每种寄存器在整个函数调用过程中的使用方法:

寄存器状态使用
RAX 易失的 返回值寄存器
RCX 易失的 第一个整型自变量
RDX 易失的 第二个整型自变量
R8 易失的 第三个整型自变量
R9 易失的 第四个整型自变量
R10:R11 易失的 必须根据需要由调用方保留;在 syscall/sysret 指令中使用
R12:R15 非易失的 必须由被调用方保留
RDI 非易失的 必须由被调用方保留
RSI 非易失的 必须由被调用方保留
RBX 非易失的 必须由被调用方保留
RBP 非易失的 可用作帧指针;必须由被调用方保留
RSP 非易失的 堆栈指针
XMM0、YMM0 易失的 第一个 FP 参数;使用 __vectorcall 时的第一个矢量类型参数
XMM1、YMM1 易失的 第二个 FP 参数;使用 __vectorcall 时的第二个矢量类型参数
XMM2、YMM2 易失的 第三个 FP 参数;使用 __vectorcall 时的第三个矢量类型参数
XMM3、YMM3 易失的 第四个 FP 自变量;使用 __vectorcall 时的第四个矢量类型参数
XMM4、YMM4 易失的 必须根据需要由调用方保留;使用 __vectorcall 时的第五个矢量类型参数
XMM5、YMM5 易失的 必须根据需要由调用方保留;使用 __vectorcall 时的第六个矢量类型参数
XMM6:XMM15、YMM6:YMM15 非易失的 (XMM),易失的(YMM 的上半部分) 必须由被调用方保留。 YMM 寄存器必须根据需要由调用方保留。

当函数进入和退出 C 运行时库调用和 Windows 系统调用时,CPU 标志寄存器的方向位标志将被清除。

 
 
[原创]关于X64程序中RUNTIME_FUNCTION,UNWIND_INFO,UNWIND_CODE结构理解 
 2021-2-3 19:27  4012

X64程序会生成一个Pdata段,用于记录每个函数的栈帧和异常信息,结构如下:
struct RUNTIME_FUNCTION
{
void __ptr32 FunctionStart;
void 
ptr32 FunctionEnd;
void *ptr32 UnwindInfo;
};
FunctionStart起始位置(RVA)
FunctionEnd结束位置(RVA)
UnwindInfo信息描述(RVA)
本人ida反汇编中RUNTIME_FUNCTION的一条信息(以下就用这条信息做介绍)
RUNTIME_FUNCTION <rva sub_180006840, rva loc_180006874, rva stru_1801FADC8>
起始位置
text:0000000180006840 sub_180006840 proc near
.text:0000000180006840 4C 89 44 24 18 mov [rsp+arg_10], r8
.text:0000000180006845 4C 89 4C 24 20 mov [rsp+arg_18], r9
.text:000000018000684A 53 push rbx
.text:000000018000684B 55 push rbp
.text:000000018000684C 56 push rsi
.text:000000018000684D 48 83 EC 40 sub rsp, 40h
.text:0000000180006851 33 DB xor ebx, ebx
结束位置(这里结束并不是函数结束)
loc_180006874: ; DATA XREF:
.text:0000000180006874 ; .rdata:00000001801FADF0↓o ...
.text:0000000180006874 48 89 7C 24 38 mov [rsp+58h+var_20], rdi

信息描述数据
stru_1801FADC8 UNWIND_INFO <1, 11h, 4, 0>

1801FADCC 11 72 UNWIND_CODE <11h, 72h> ; UWOP_ALLOC_SMALL
1801FADCE 0D 60 UNWIND_CODE <0Dh, 60h> ; UWOP_PUSH_NONVOL
1801FADD0 0C 50 UNWIND_CODE <0Ch, 50h> ; UWOP_PUSH_NONVOL
1801FADD2 0B 30 UNWIND_CODE <0Bh, 30h> ; UWOP_PUSH_NONVOL
信息描述是一个UNWIND_INFO结构体,其中第三个参数4代表有四个UNWIND_CODE 数据,UNWIND_CODE可以理解成伪指令,用于描述这段代码对栈的操作,第一个UNWIND_CODE是在栈上分配内存(sub rsp,*),11代表起始位置偏移,也就是(180006840+11),地址0x180006851这条地址的上一条指令就是sub rsp,40h,第二个UNWIND_CODE的0D是可以理解成伪指令,对应的操作是push rsi,第三条OC代表push rbp,第四条 0B代表push rbx,所以X86没有像X86一样通过ebp来进行栈回溯,而是通过.pdata段中的RUNNING_FUNCTION 直接找到栈中的返回地址,从而实现栈回溯,编译器已经把所有函数的结构生成在PE文件中
以上如有错误希望大佬指出   


X64 SEH的展开

C++ 代码:

#include "stdafx.h"
#include <windows.h>

ULONG WINAPI FilterFunc(DWORD dwExceptionCode)
{
	return (dwExceptionCode == STATUS_INTEGER_DIVIDE_BY_ZERO) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH;
}

int add1(int a,int b)
{
	int c = 0,d=0;
	__try
	{
		printf("a address=%p\n",&a);
		__try
		{
			printf("b address=%p\n", &b);
			c = a / b;
		}
		__except (FilterFunc(GetExceptionCode()))
		{
			printf("c address=%p\n", &c);
			c = a / (a + b);
		}

	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		printf("d address=%p\n", &d);
		c = c + 1;
	}

	return c;
}

#include <stdio.h> 
#include <Windows.h> 
int main()
{
	DebugBreak();
	add1(0,0);
	return 0;
}

1 X64 的基本概念和结构体

相比于X86 在程序运行中动态构建SEH结构,X64-SEH 是静态的,其信息包含在PE文件中。下面我们首先看一下对应的结构,然后看看其提供的信息是否能够满足异常捕获以及异常处理的功能。

为异常处理和调试器支持展开数据

执行异常处理和调试支持所需的数据结构

RUNTIME_FUNCTION
typedef struct _RUNTIME_FUNCTION_ { 
    DWORD BeginAddress;                             // 函数起始地址 
    DWORD EndAddress;                                // 函数结束地址 
    DWORD UnwindInfoAddress;               // 展开信息地址看下面的_UNWIND_INFO 结构体    
}RUNTIME_FUNCTION , *_RUNTIME_FUNCTION ;
该结构在内存中必须为DWORD 对齐。所有的地址都是ImageBase 的 RVA 值。这些项已经经过排序了(按照BeginAddress升序排列),放置在PE32+ 的 .pdata节中。对于动态生成的函数,我们暂时不介绍。
UNWIND_INFO
// 
// Define unwind information flags. 
//
#define UNW_FLAG_NHANDLER 0x0 
#define UNW_FLAG_EHANDLER 0x1 
#define UNW_FLAG_UHANDLER 0x2 
#define UNW_FLAG_CHAININFO 0x4
上面四个标志依次代表:
既没有EXCEPT_FILTER也没有EXCEPT_HANDLER
有EXCEPT_FILTER 和 EXCEPT_HANDLER
有 FINALLY_HANDLER
有多个UNWIND_INFO 串联在一起,
typedef struct _UNWIND_INFO { 
    UCHAR Version : 3;                     // 版本信息,当前为1 
    UCHAR Flags : 5;                          // 对应上面的四个标志 
    UCHAR SizeOfProlog;                 // Prolog 的大小,单位是字节 
    UCHAR CountOfCodes;              // UNWIND_INFO 包含多少UNWIND_CODE结构 
    UCHAR FrameRegister : 4;   
    UCHAR FrameOffset : 4;  
    UNWIND_CODE UnwindCode[1];
// 
// unwind codes 数组后面是一个可选的DWROD 对齐的成员。此成员有两种可能,异常处理函数地址或者function table entry(flags中指定了UNW_FALGS_CHAININFO),如果是异常处理函数地址的话,它将为一个语言相关的异常处理数据 
// 
  union { 
      struct { //下面两个组成一个结构体,不是联合体,看清楚
          ULONG ExceptionHandler; 
          ULONG ExceptionData[]; 
      }; 
 
      RUNTIME_FUNCTION FunctionEntry; //如果上面的Flags指定的是 UNW_FLAG_CHAININFO,该联合体为 RUNTIME_FUNCTION
  }; 

} UNWIND_INFO, *PUNWIND_INFO;
UNWIND_INFO 结构体必须是DWORD 对齐的。
FrameRegister
如果不是0 的话,这个函数使用了帧指针,该值表示帧指针使用的非易失性寄存器的数目。与UNWIND_CODE中的成员使用相同的编码。
FrameOffset
如果FrameRegister 不为0,表示在刚建立栈帧时,应用于FP 寄存器的RSP 的缩放偏移量。实际的FP 为RSP+16*当前值,范围是0~240。这样允许将FP 寄存器指向本地动态栈帧的中间位置,然后通过更短的指令来提高代码密度(更多的 指令将可以使用8位带符号偏移形式)。
Unwind code 数组
这一系列的code 代表了prolog 中对于非易失性寄存器和RSP 寄存器的影响。由于对齐, 这个数组始终有偶数个,最后的一个成员可能未被使用。
Exception Handler
ExceptionHandler 该域为一个RVA,指向exception handler的地址,ExceptionData 指向一个类似于scopetable 的地址
如果Flags指定的是 UNW_FLAG_CHAININFO,该域为 RUNTIME_FUNCTION
UNWIND_CODE
typedef enum _UNWIND_OP_CODES {  
    UWOP_PUSH_NONVOL = 0, /* info == register number */  
    UWOP_ALLOC_LARGE,     /* no info, alloc size in next 2 slots */  
    UWOP_ALLOC_SMALL,     /* info == size of allocation / 8 - 1 */  
    UWOP_SET_FPREG,       /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */  
    UWOP_SAVE_NONVOL,     /* info == register number, offset in next slot */  
    UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */  
    UWOP_SAVE_XMM128,     /* info == XMM reg number, offset in next slot */  
    UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */  
    UWOP_PUSH_MACHFRAME   /* info == 0: no error-code, 1: error-code */  
} UNWIND_CODE_OPS;  
  
typedef union _UNWIND_CODE {  
    struct {  
        UBYTE CodeOffset;  
        UBYTE UnwindOp : 4;  
        UBYTE OpInfo   : 4;  
    };  
    USHORT FrameOffset;  
} UNWIND_CODE, *PUNWIND_CODE;  
UNWIND_CODE结构体用来记录在proglog 中影响非易失性寄存器和RSP寄存器的序列。每个UNWIND_CODE有上述结构。其中,分别表示本操作在prolog中的offset,Unwind操作码,操作信息。该数组的排列按照prolog中的offset的降序排列。
 
 有些展开操作代码需要本地栈帧的一个无符号偏移。这个偏移是相对于固定栈申请而言的。如果UNWIND_INFO的Frame Register 成员为0,offset 是对RSP而言的,否则,offset 是相对于在建立栈帧的时候RSP 被存储的的地方。此时需要栈帧-栈帧寄存器的偏移(16*缩放帧寄存器在UNWIND_INFO中的偏移)。如果FP 寄存器被使用,所有使用offset的unwind code必须在prolog建立栈帧之后才可以使用。

2  查找add1函数的RUNTIME_FUNCTION

 使用windbg 

0:000> .fnent ConsoleApplication3!add1
Debugger function entry 00000000`003a0fc0 for:
d:\work\temp\consoleapplication3\consoleapplication3.cpp(14)
(00000001`3f501030)   ConsoleApplication3!add1   |  (00000001`3f5010e0)   ConsoleApplication3!main
Exact matches:
    ConsoleApplication3!add1 (int, int)


BeginAddress      = 00000000`00001030
EndAddress        = 00000000`000010d4
UnwindInfoAddress = 00000000`00002670


Unwind info at 00000001`3f502670, 10 bytes
  version 1, flags 1, prolog c, codes 1
  handler routine: ConsoleApplication3!_C_specific_handler (00000001`3f501e10), data 2
  00: offs c, unwind op 2, op info 8	UWOP_ALLOC_SMALL.

根据上面的信息,结合Study_PE+,从.pdata section中找到对应的信息 

2 .pdada Section中寻找add1 UNW_INFO的信息

我们知道add1的UNW_INFO 的RVA是2670,所以我们使用PE工具得到FOA

3 add1的 UNW_INFO 解析

000000001A70: 09 0C 01 00

09的二进制00001 001 ,Flags:5  = 00001 ,Version:3 = 001 也就是说版本是1 。Flags是1,也就是#define UNW_FLAG_EHANDLER 0x1 。

0C 也就是prelog 占用的大小为0c , 可以用windbg看下,

     

00000001`3f501030 89542410        mov     dword ptr [rsp+10h],edx
00000001`3f501034 894c2408        mov     dword ptr [rsp+8],ecx
00000001`3f501038 4883ec48        sub     rsp,48h
prelog占用C大小

01  仅仅包含一个UNWIND_INFO 结构。需要注意的是unwind_info数组通常都是偶数出现,为了对齐,另外unwind_info是2字节的union联合体,所以这个例子应该占用4个字节,后两个字节用00 00 对齐

00  

    UCHAR FrameRegister : 4;   
    UCHAR FrameOffset : 4;  

 

000000001A74: 0C 82 00 00 

对应的是UNWIND_CODE结构,可以参考https://msdn.microsoft.com/zh-cn/library/ck9asaa9.aspx

CodeOffset为0C,即偏移为0C

UnwinOp为2 ,即UWOP_ALLOC_SMALL

OpInfo为8,即申请大小为(08+1)*8 = 72 =0h48

可以反汇编验证一下

0:000> u ConsoleApplication3!add1
ConsoleApplication3!add1 [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 14]:
00000001`3fc71030 89542410        mov     dword ptr [rsp+10h],edx
00000001`3fc71034 894c2408        mov     dword ptr [rsp+8],ecx
00000001`3fc71038 4883ec48        sub     rsp,48h

 此外我们也验证一下main函数的

:000> .fnent ConsoleApplication3!main
Debugger function entry 00000000`003a5140 for:
d:\work\temp\consoleapplication3\consoleapplication3.cpp(43)
(00000001`3fc710e0)   ConsoleApplication3!main   |  (00000001`3fc71100)   ConsoleApplication3!__vcrt_va_start_verify_argument_type<char const * __ptr64 const>
Exact matches:
    ConsoleApplication3!main (void)

BeginAddress      = 00000000`000010e0
EndAddress        = 00000000`000010fa
UnwindInfoAddress = 00000000`000026a8

Unwind info at 00000001`3fc726a8, 6 bytes
  version 1, flags 0, prolog 4, codes 1
  00: offs 4, unwind op 2, op info 4	UWOP_ALLOC_SMALL.//栈空间大小为(4+1)*8 =40 =0h28

0:000> u ConsoleApplication3!main
ConsoleApplication3!main [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 43]:
00000001`3fc710e0 4883ec28        sub     rsp,28h//栈空间大小为0h28
00000001`3fc710e4 ff15160f0000    call    qword ptr [ConsoleApplication3!_imp_DebugBreak (00000001`3fc72000)]
00000001`3fc710ea 33d2            xor     edx,edx
00000001`3fc710ec 33c9            xor     ecx,ecx
00000001`3fc710ee e83dffffff      call    ConsoleApplication3!add1 (00000001`3fc71030)
00000001`3fc710f3 33c0            xor     eax,eax
00000001`3fc710f5 4883c428        add     rsp,28h 
00000001`3fc710f9 c3              ret

剩下的00 00是为了对齐

unwind_info 和unwind_code主要为了发生异常的时候,SEH可以根据这个信息进行栈回滚。

   这样在x64 中,MSC 为几乎所有的函数都登记了完备的信息,用来在展开过程中完整的回滚函数所做的栈、寄存器操作。登记的信息包括:
       函数是否使用了 SEH、
       函数使用的是什么组合的 SEH(__try/__except?__try/__finally?)、
       函数申请了多少栈空间、
       函数保存了哪些寄存器、
       函数是否建立了栈帧,
       等等,
       同时也记录了这些操作的顺序(以保证回滚的时候不会乱套)。

000000001A78: 10 1E 00 00

这里是一个异常处理函数的RVA,我们可用windbg断下看下。

步骤 

0:000> lmvm ConsoleApplication3
Browse full module list
start             end                 module name
00000001`3fc70000 00000001`3fc78000   ConsoleApplication3 C (private pdb symbols)  D:\work\temp\ConsoleApplication3\x64\Release\ConsoleApplication3.pdb
    Loaded symbol image file: D:\work\temp\ConsoleApplication3\x64\Release\ConsoleApplication3.exe
    Image path: D:\work\temp\ConsoleApplication3\x64\Release\ConsoleApplication3.exe
    Image name: ConsoleApplication3.exe
    Browse all global symbols  functions  data
    Timestamp:        Thu Dec 07 23:40:44 2017 (5A2960FC)
    CheckSum:         00000000
    ImageSize:        00008000
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4 

 对RVA异常函数加断点

0:000> bp 00000001`3fc70000 + 1e10
当发生异常的时候就会断下 
0:000> g
(1458.210c): Integer divide-by-zero - code c0000094 (first chance)//这是DIV 0的第一次机会
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
ConsoleApplication3!add1+0x44:
00000001`3fc71074 f77c2458        idiv    eax,dword ptr [rsp+58h] ss:00000000`002dfa18=00000000
0:000> g
Breakpoint 0 hit //断点断下
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\SYSTEM32\ntdll.dll - 
ConsoleApplication3!_C_specific_handler:
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\system32\VCRUNTIME140.dll - 
00000001`3fc71e10 ff257a020000    jmp     qword ptr [ConsoleApplication3!_imp___C_specific_handler (00000001`3fc72090)] ds:00000001`3fc72090={VCRUNTIME140!_C_specific_handler (000007fe`f076bff0)}
0:000> kn
 # Child-SP          RetAddr           Call Site
00 00000000`002deba8 00000000`77c9812d ConsoleApplication3!_C_specific_handler
01 00000000`002debb0 00000000`77c8855f ntdll!RtlDecodePointer+0xad
02 00000000`002debe0 00000000`77cbbcb8 ntdll!RtlUnwindEx+0xbbf
03 00000000`002df2c0 00000001`3fc71074 ntdll!KiUserExceptionDispatcher+0x2e
04 00000000`002df9c0 00000001`3fc710f3 ConsoleApplication3!add1+0x44 [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 22]
05 00000000`002dfa10 00000001`3fc71409 ConsoleApplication3!main+0x13 [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 46]
06 (Inline Function) --------`-------- ConsoleApplication3!invoke_main+0x22 [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 64]
07 00000000`002dfa40 00000000`77a659cd ConsoleApplication3!__scrt_common_main_seh+0x11d [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 253]
08 00000000`002dfa80 00000000`77c9a561 kernel32!BaseThreadInitThunk+0xd
09 00000000`002dfab0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

__C_specific_handler function

Called by the compiler to implement structured exception handling extensions.

The relative address of the language specific handler is present in the UNWIND_INFO whenever flags UNW_FLAG_EHANDLER or UNW_FLAG_UHANDLER are set. The language specific handler is called as part of the search for an exception handler or as part of an unwind. For more information see Language Specific Handler.

_CRTIMP  __C_specific_handler(
  _In_    struct _EXCEPTION_RECORD   *ExceptionRecord,
  _In_    void                       *EstablisherFrame,
  _Inout_ struct _CONTEXT            *ContextRecord,
  _Inout_ struct _DISPATCHER_CONTEXT *DispatcherContext
); 
0:000> r
rax=0000000000000000 rbx=00000000002e0000 rcx=00000000002df7b0
rdx=00000000002df9c0 rsi=000000013fc7400c rdi=00000000002dd000
rip=000000013fc71e10 rsp=00000000002deba8 rbp=00000000002df9c0
 r8=00000000002df2c0  r9=00000000002dec70 r10=000000013fc71e10
r11=000000013fc74000 r12=000000013fc71074 r13=0000000000000000
r14=00000000002df7b0 r15=000000013fc70000
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ConsoleApplication3!_C_specific_handler:
00000001`3fc71e10 ff257a020000    jmp     qword ptr [ConsoleApplication3!_imp___C_specific_handler (00000001`3fc72090)] ds:00000001`3fc72090={VCRUNTIME140!_C_specific_handler (000007fe`f076bff0)}
0:000> dt 00000000002df7b0 _EXCEPTION_RECORD
ConsoleApplication3!_EXCEPTION_RECORD
   +0x000 ExceptionCode    : 0xc0000094
   +0x004 ExceptionFlags   : 0
   +0x008 ExceptionRecord  : (null) 
   +0x010 ExceptionAddress : 0x00000001`3fc71074 Void //异常地址
   +0x018 NumberParameters : 0
   +0x020 ExceptionInformation : [15] 0x3fb95b
0:000> !error 0xc0000094  //好像没有显示成功,查找网络

0xC0000094: Integer division by zero

Error code: (NTSTATUS) 0xc0000094 (3221225620) - {
0:000> u 0x00000001`3fc71074
ConsoleApplication3!add1+0x44 [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 22]:
00000001`3fc71074 f77c2458        idiv    eax,dword ptr [rsp+58h] //这里处理问题
00000001`3fc71078 89442420        mov     dword ptr [rsp+20h],eax
00000001`3fc7107c eb30            jmp     ConsoleApplication3!add1+0x7e (00000001`3fc710ae)
00000001`3fc7107e 488d542420      lea     rdx,[rsp+20h]
00000001`3fc71083 488d0da6110000  lea     rcx,[ConsoleApplication3!GS_ExceptionPointers+0x30 (00000001`3fc72230)]
00000001`3fc7108a e8e1000000      call    ConsoleApplication3!printf (00000001`3fc71170)
00000001`3fc7108f 8b442458        mov     eax,dword ptr [rsp+58h]
00000001`3fc71093 8b4c2450        mov     ecx,dword ptr [rsp+50h]
此时我们需要知道此时的寄存器等信息,这个保存在了第三个参数(X64 参数顺序 rcx rdx r8 r9),
0:000> dt _CONTEXT @r8
ConsoleApplication3!_CONTEXT
   +0x000 P1Home           : 0x2df7b0
   +0x008 P2Home           : 0x2df2c0
   +0x010 P3Home           : 0
   +0x018 P4Home           : 0
   +0x020 P5Home           : 0x000007fe`f3ac55f0
   +0x028 P6Home           : 0x3fb940
   +0x030 ContextFlags     : 0x10005f
   +0x034 MxCsr            : 0x1f80
   +0x038 SegCs            : 0x33
   +0x03a SegDs            : 0x2b
   +0x03c SegEs            : 0x2b
   +0x03e SegFs            : 0x53
   +0x040 SegGs            : 0x2b
   +0x042 SegSs            : 0x2b
   +0x044 EFlags           : 0x10206
   +0x048 Dr0              : 0
   +0x050 Dr1              : 0
   +0x058 Dr2              : 0
   +0x060 Dr3              : 0
   +0x068 Dr6              : 0
   +0x070 Dr7              : 0
   +0x078 Rax              : 0
   +0x080 Rcx              : 0x000007fe`f3ac4198
   +0x088 Rdx              : 0
   +0x090 Rbx              : 0x000007fe`f3ac59f4
   +0x098 Rsp              : 0x2df9c0
   +0x0a0 Rbp              : 0
   +0x0a8 Rsi              : 0
   +0x0b0 Rdi              : 0x000007fe`f3ac5a10
   +0x0b8 R8               : 0x2ddc98
   +0x0c0 R9               : 0x3fb95b
   +0x0c8 R10              : 0
   +0x0d0 R11              : 0x2df890
   +0x0d8 R12              : 0
   +0x0e0 R13              : 0
   +0x0e8 R14              : 0
   +0x0f0 R15              : 0
   +0x0f8 Rip              : 0x00000001`3fc71074
   +0x100 FltSave          : _XSAVE_FORMAT
   +0x100 Header           : [2] _M128A
   +0x120 Legacy           : [8] _M128A
   +0x1a0 Xmm0             : _M128A
   +0x1b0 Xmm1             : _M128A
   +0x1c0 Xmm2             : _M128A
   +0x1d0 Xmm3             : _M128A
   +0x1e0 Xmm4             : _M128A
   +0x1f0 Xmm5             : _M128A
   +0x200 Xmm6             : _M128A
   +0x210 Xmm7             : _M128A
   +0x220 Xmm8             : _M128A
   +0x230 Xmm9             : _M128A
   +0x240 Xmm10            : _M128A
   +0x250 Xmm11            : _M128A
   +0x260 Xmm12            : _M128A
   +0x270 Xmm13            : _M128A
   +0x280 Xmm14            : _M128A
   +0x290 Xmm15            : _M128A
   +0x300 VectorRegister   : [26] _M128A
   +0x4a0 VectorControl    : 0x00000020`00001000
   +0x4a8 DebugControl     : 0x1000000
   +0x4b0 LastBranchToRip  : 0
   +0x4b8 LastBranchFromRip : 0
   +0x4c0 LastExceptionToRip : 0
   +0x4c8 LastExceptionFromRip : 0
+0x078 Rax              : 0
   +0x098 Rsp              : 0x2df9c0
0:000> dq 0x2df9c0+58 L1
00000000`002dfa18  00000000`00000000

EAX/0 导致异常。

剩下的是SCOPE_TABLE结构

000000001A7C: 02 00 00 00

表示有两个ScopeRecord,剩下的数据是这两个ScopeRecord数据

000000001A80: 5E 10 00 00 7E 10 00 00 D0 1E 00 00 7E 10 00 00

第一个ScopeRecord 的对应关系

我们对Handler下断点

0:000> bp 00000001`3fc70000+ 1ed0
0:000> g
Breakpoint 1 hit
ConsoleApplication3!`add1'::`1'::filt$0:
00000001`3fc71ed0 4055            push    rbp
0:000> k
 # Child-SP          RetAddr           Call Site
00 00000000`002deb38 000007fe`f076c090 ConsoleApplication3!`add1'::`1'::filt$0 [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 24]
01 00000000`002deb40 00000000`77c9812d VCRUNTIME140!_C_specific_handler+0xa0
02 00000000`002debb0 00000000`77c8855f ntdll!RtlDecodePointer+0xad
03 00000000`002debe0 00000000`77cbbcb8 ntdll!RtlUnwindEx+0xbbf
04 00000000`002df2c0 00000001`3fc71074 ntdll!KiUserExceptionDispatcher+0x2e
05 00000000`002df9c0 00000001`3fc710f3 ConsoleApplication3!add1+0x44 [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 22]
06 00000000`002dfa10 00000001`3fc71409 ConsoleApplication3!main+0x13 [d:\work\temp\consoleapplication3\consoleapplication3.cpp @ 46]
07 (Inline Function) --------`-------- ConsoleApplication3!invoke_main+0x22 [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 64]
08 00000000`002dfa40 00000000`77a659cd ConsoleApplication3!__scrt_common_main_seh+0x11d [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 253]
09 00000000`002dfa80 00000000`77c9a561 kernel32!BaseThreadInitThunk+0xd
0a 00000000`002dfab0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
发现会跳转到我们的处理函数中

000000001A90: 4C 10 00 00 B0 10 00 00 FB 1E 00 00 B0 10 00 00

第二个ScopeRecord可以同样分析。

C++对应的汇编

ConsoleApplication3!add1:
00000001`3fc71030 89542410        mov     dword ptr [rsp+10h],edx
00000001`3fc71034 894c2408        mov     dword ptr [rsp+8],ecx
00000001`3fc71038 4883ec48        sub     rsp,48h
00000001`3fc7103c c744242000000000 mov     dword ptr [rsp+20h],0
00000001`3fc71044 c744242c00000000 mov     dword ptr [rsp+2Ch],0
00000001`3fc7104c 488d542450      lea     rdx,[rsp+50h]
00000001`3fc71051 488d0db8110000  lea     rcx,[ConsoleApplication3!GS_ExceptionPointers+0x10 (00000001`3fc72210)]
00000001`3fc71058 e813010000      call    ConsoleApplication3!printf (00000001`3fc71170)
00000001`3fc7105d 90              nop
00000001`3fc7105e 488d542458      lea     rdx,[rsp+58h]
00000001`3fc71063 488d0db6110000  lea     rcx,[ConsoleApplication3!GS_ExceptionPointers+0x20 (00000001`3fc72220)]
00000001`3fc7106a e801010000      call    ConsoleApplication3!printf (00000001`3fc71170)
00000001`3fc7106f 8b442450        mov     eax,dword ptr [rsp+50h]
00000001`3fc71073 99              cdq
00000001`3fc71074 f77c2458        idiv    eax,dword ptr [rsp+58h]
00000001`3fc71078 89442420        mov     dword ptr [rsp+20h],eax
00000001`3fc7107c eb30            jmp     ConsoleApplication3!add1+0x7e (00000001`3fc710ae)
00000001`3fc7107e 488d542420      lea     rdx,[rsp+20h]
00000001`3fc71083 488d0da6110000  lea     rcx,[ConsoleApplication3!GS_ExceptionPointers+0x30 (00000001`3fc72230)]
00000001`3fc7108a e8e1000000      call    ConsoleApplication3!printf (00000001`3fc71170)
00000001`3fc7108f 8b442458        mov     eax,dword ptr [rsp+58h]
00000001`3fc71093 8b4c2450        mov     ecx,dword ptr [rsp+50h]
00000001`3fc71097 03c8            add     ecx,eax
00000001`3fc71099 8bc1            mov     eax,ecx
00000001`3fc7109b 89442428        mov     dword ptr [rsp+28h],eax
00000001`3fc7109f 8b442450        mov     eax,dword ptr [rsp+50h]
00000001`3fc710a3 99              cdq
00000001`3fc710a4 8b4c2428        mov     ecx,dword ptr [rsp+28h]
00000001`3fc710a8 f7f9            idiv    eax,ecx
00000001`3fc710aa 89442420        mov     dword ptr [rsp+20h],eax
00000001`3fc710ae eb1b            jmp     ConsoleApplication3!add1+0x9b (00000001`3fc710cb)
00000001`3fc710b0 488d54242c      lea     rdx,[rsp+2Ch]
00000001`3fc710b5 488d0d84110000  lea     rcx,[ConsoleApplication3!GS_ExceptionPointers+0x40 (00000001`3fc72240)]
00000001`3fc710bc e8af000000      call    ConsoleApplication3!printf (00000001`3fc71170)
00000001`3fc710c5 ffc0            inc     eax
00000001`3fc710c7 89442420        mov     dword ptr [rsp+20h],eax
00000001`3fc710cb 8b442420        mov     eax,dword ptr [rsp+20h]
00000001`3fc710cf 4883c448        add     rsp,48h
00000001`3fc710d3 c3              ret
00000001`3fc710d4 cc              int     3
00000001`3fc710d5 cc              int     3
00000001`3fc710d6 cc              int     3
00000001`3fc710d7 cc              int     3
00000001`3fc710d8 cc              int     3
00000001`3fc710d9 cc              int     3
00000001`3fc710da cc              int     3
00000001`3fc710db cc              int     3
00000001`3fc710dc cc              int     3
00000001`3fc710dd cc              int     3
00000001`3fc710de cc              int     3

扩展话题:

windbg 在X64的栈回溯,也是利用了RUNTIME_FUNCTION的信息

1 得到函数的任意RIP - EXE的基地址的地址差值RVA,然后遍历.pdata找到这个差值所在的RUNTIME_FUNCTION信息,得到本函数的栈空间大小,可以直接使用.fnent @rip

2 根据上面函数的返回地址替代RIP,重复上面的第一个步骤

3 重复上面所有步骤就可以得到X64的栈.

参考:

https://www.cnblogs.com/lanrenxinxin/p/4762858.html

http://blog.csdn.net/qq_18218335/article/details/72722320

后补:

C++代码解析SEH

http://blog.csdn.net/Shevacoming/article/details/7826527


x64 堆栈使用

所有超出 RSP 当前地址的内存都被视为易失性内存:操作系统或调试器可能会在用户调试会话或中断处理程序期间覆盖此内存。
因此,在尝试对堆栈帧读取或写入值之前,必须始终设置 RSP。

All memory beyond the current address of RSP is considered volatile:
The OS, or a debugger, may overwrite this memory during a user debug session, or an interrupt handler.
Thus, RSP must always be set before attempting to read or write values to a stack frame.

本节讨论局部变量的堆栈空间分配和 alloca 内部函数。

堆栈分配

函数的 prolog 负责为局部变量、保存的寄存器、堆栈参数和寄存器参数分配堆栈空间。

参数区域始终位于堆栈底部(即使使用 alloca),以便它在任何函数调用期间都始终与返回地址相邻。 它至少包含四个条目,但始终包含足够的空间来保存任何可能被调用的函数所需的所有参数。 请注意,系统始终为寄存器参数分配空间,即使这些参数本身从不驻留在堆栈中;会向被调用方保证为其所有参数分配了空间。 寄存器参数需要主地址,因此,如果所调用的函数需要获取参数列表 (va_list) 或单独参数的地址,则可使用连续区域。 此区域还提供了一个方便位置,用于在 thunk 执行期间保存寄存器参数,并作为调试选项(例如,如果参数存储在 prolog 代码中的主地址处,则可以在调试期间轻松查找参数)。 即使所调用的函数的参数少于 4 个,这 4 个堆栈位置实际上由所调用的函数所拥有,并且可能会由所调用的函数用于除保存参数寄存器值之外的其他用途。 因此,调用方可能不会在函数调用中将信息保存在此堆栈区域中。

如果在函数中动态分配空间 (alloca),则必须使用非易失性寄存器作为帧指针来标记堆栈固定部分的基址,并且必须在 prolog 中保存和初始化该寄存器。 请注意,使用 alloca 时,从同一个调用方对同一个被调用方进行的调用对于其寄存器参数可能具有不同的主地址。

堆栈将始终保持为 16 字节对齐,除非是在 prolog 中(例如,压入返回地址之后),以及除非在某类帧函数的函数类型中指明。

下面是堆栈布局的一个示例,其中函数 A 调用非叶函数 B。函数 A 的 prolog 已在堆栈底部为 B 所需的所有寄存器和堆栈参数分配了空间。 此调用会压入返回地址,而 B 的 prolog 会为其局部变量、非易失性寄存器以及它调用函数所需的空间分配空间。 如果 B 使用 alloca,则在局部变量/非易失性寄存器保存区域与参数堆栈区域之间分配空间。

x64 转换示例的堆积布局的关系图。

当函数 B 调用另一个函数时,返回地址会压入到 RCX 的主地址正下方。

动态参数堆栈区域构造

如果使用帧指针,则可选择动态创建参数堆栈区域。 当前在 x64 编译器中未实现此操作。

函数类型

主要有两种类型的函数。 需要堆栈帧的函数称为帧函数 。 不需要堆栈帧的函数称为叶函数 。

帧函数是分配堆栈空间、调用其他函数、保存非易失性寄存器或使用异常处理的函数。 它还需要函数表条目。 帧函数需要 prolog和 epilog。 帧函数可以动态分配堆栈空间,并可以使用帧指针。 帧函数可自行使用此调用标准的所有功能。

如果帧函数不调用另一个函数,则不需要使堆栈对齐(在堆栈分配一节中涉及)。

叶函数是不需要函数表条目的函数。 它无法更改任何非易失性寄存器(包括 RSP),这意味着它无法调用任何函数或分配堆栈空间。 它在执行时可以使堆栈保持未对齐状态。

malloc 对齐

malloc 保证返回适当对齐的内存,用于存储任何具有基本对齐以及可以适合所分配的内存量的对象。 基本对齐 是在没有对齐规范的情况下,小于或等于实现所支持的最大对齐的对齐。 (在 Visual C++ 中,这是 double 或 8 字节所需的对齐方式。在面向 64 位平台的代码中,则是 16 字节。)例如,4 字节分配将在支持任何 4 字节或更小对象的边界上对齐。

Visual C++ 允许使用具有扩展对齐 的类型,这些类型也称为过度对齐 类型。 例如,SSE 类型 __m128 和 __m256 以及使用 __declspec(align( n ))(其中 n 大于 8)声明的类型具有扩展对齐。 malloc 不保证内存在适合于需要扩展对齐的对象的边界上对齐。 若要为过度对齐类型分配内存,请使用 _aligned_malloc 和相关函数。

alloca

_alloca 需要是 16 字节对齐,此外需要使用帧指针。

分配的堆栈需要在它后面包含用于后续调用函数的参数的空间,如堆栈分配中所述。

请参阅

x64 软件约定

 


 

 

x64 异常处理

x64 上的结构化异常处理和 C++ 异常处理编码约定和行为的概述。 有关异常处理的常规信息,请参阅 Visual C++ 中的异常处理

为异常处理和调试器支持展开数据

异常处理和调试支持需要几个数据结构。

RUNTIME_FUNCTION 结构

基于表的异常处理要求分配堆栈空间或调用另一个函数(例如非叶函数)的所有函数都有一个表条目。 函数表条目的格式为:

大小
ULONG 函数起始地址
ULONG 函数结束地址
ULONG 展开信息地址

RUNTIME_FUNCTION 结构必须在内存中为 DWORD 对齐。 所有地址都相对于映像,也就是说,它们是相对于包含函数表条目的映像起始地址的 32 位偏移。 这些条目会进行排序,并放入 PE32+ 映像的 .pdata 节中。 对于动态生成的函数 [JIT 编译器],支持这些函数的运行时必须使用 RtlInstallFunctionTableCallback 或 RtlAddFunctionTable 将此信息提供给操作系统。 否则将导致不可靠的异常处理和进程调试。

UNWIND_INFO 结构

展开数据信息结构用于记录函数对堆栈指针的影响,以及非易失寄存器在堆栈上保存的位置:

大小
UBYTE:3 Version
UBYTE:5 Flags
UBYTE prolog 的大小
UBYTE 展开代码的计数
UBYTE:4 帧寄存器
UBYTE:4 帧寄存器偏移(比例)
USHORT * n 展开代码数组
变量 可以采用下面的形式 (1) 或 (2)

(1) 异常处理程序

大小
ULONG 异常处理程序的地址
变量 特定于语言的处理程序数据(可选)

(2) 链式展开信息

大小
ULONG 函数起始地址
ULONG 函数结束地址
ULONG 展开信息地址

UNWIND_INFO 结构必须在内存中为 DWORD 对齐。 下面是每个字段的含义:

  • Version

    展开数据的版本号,当前为 1。

  • 标记

    当前定义了三个标志:

    Flag描述
    UNW_FLAG_EHANDLER 函数具有一个异常处理程序,应在查找需要检查异常的函数时调用该处理程序。
    UNW_FLAG_UHANDLER 函数具有一个终止处理程序,应在展开异常时调用该处理程序。
    UNW_FLAG_CHAININFO 此展开信息结构不是过程的主结构。 相反,链式展开信息条目是上一个 RUNTIME_FUNCTION 条目的内容。 有关信息,请参阅链式展开信息结构。 如果设置了此标志,则必须清除 UNW_FLAG_EHANDLER 和 UNW_FLAG_UHANDLER 标志。 此外,帧寄存器和固定堆栈分配字段必须具有与主展开信息中相同的值。
  • prolog 的大小

    函数 prolog 的长度(以字节为单位)。

  • 展开代码的计数

    展开代码数组中的槽数。 某些展开代码(例如 UWOP_SAVE_NONVOL)要求数组中有多个槽。

  • 帧寄存器

    如果为非零值,则函数使用帧指针 (FP),并且此字段是用作帧指针的非易失性寄存器编号(对 UNWIND_CODE 节点的操作信息字段使用相同的编码)。

  • 帧寄存器偏移(比例)

    如果帧寄存器字段为非零值,则此字段是相对于在建立时应用于 FP 寄存器的 RSP 的比例偏移。 实际 FP 寄存器设置为 RSP + 16 * 此数字,允许 0 到 240 的偏移。 此偏移允许将 FP 寄存器指向动态堆栈帧的本地堆栈分配中间,从而可通过更短的指令实现更好的代码密度。 (也就是说,更多指令可以使用 8 位有符号偏移形式。)

  • 展开代码数组

    说明 prolog 对非易失性寄存器和 RSP 的影响的项的数组。 有关各个项的含义,请参阅有关 UNWIND_CODE 的章节。 出于对齐目的,此数组始终具有偶数数量的条目,最后一个条目可能未使用。 在这种情况下,数组的长度会超过展开代码字段计数所指示的长度。

  • 异常处理程序的地址

    相对于映像的指针,指向函数特定于语言的异常或终止处理程序(如果清除了标志 UNW_FLAG_CHAININFO,并且设置了标志 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER 中的一个)。

  • 特定于语言的处理程序数据

    函数特定于语言的异常处理程序数据。 此数据的格式未指定,完全由所使用的特定异常处理程序确定。

  • 链式展开信息

    如果设置了标志 UNW_FLAG_CHAININFO,则 UNWIND_INFO 结构以三个 UWORD 结尾。 这些 UWORD 表示链式展开函数的 RUNTIME_FUNCTION 信息。

UNWIND_CODE 结构

展开代码数组用于记录 prolog 中影响非易失性寄存器和 RSP 的操作序列。 每个代码项都具有以下格式:

大小
UBYTE prolog 中的偏移
UBYTE:4 展开操作代码
UBYTE:4 操作信息

数组按 prolog 中偏移的降序排序。

prolog 中的偏移

执行此操作的指令结尾的偏移(相对于 prolog 的开头)加 1(即下一个指令开头的偏移)。

展开操作代码

注意:某些操作代码需要本地堆栈帧中的值的无符号偏移。 此偏移相对于开头(即固定堆栈分配的最低地址)。 如果 UNWIND_INFO 中的帧寄存器字段为零,则此偏移相对于 RSP。 如果帧寄存器字段为非零值,则此偏移相对于建立 FP 寄存器时 RSP 所处的位置。 它等于 FP 寄存器减去 FP 寄存器偏移(16 * UNWIND_INFO 中的比例帧寄存器偏移)。 如果使用 FP 寄存器,则必须仅在 prolog 中建立 FP 寄存器之后,才能使用任何采用偏移的展开代码。

对于除 UWOP_SAVE_XMM128 和 UWOP_SAVE_XMM128_FAR 之外的所有操作码,偏移始终为 8 的倍数,因为所有相关堆栈值都存储在 8 字节边界上(堆栈本身始终为 16 字节对齐)。 对于采用短偏移(小于 512K)的操作码,此代码的节点中的最后一个 USHORT 会保存偏移除以 8 的值。 对于采用长偏移(512K < = 偏移 < 4GB)的操作代码,此代码的最后两个 USHORT 节点会保存偏移(采用 little-endian 格式)。

对于操作码 UWOP_SAVE_XMM128 和 UWOP_SAVE_XMM128_FAR,偏移始终为 16 的倍数,因为所有 128 位 XMM 操作都必须在 16 字节对齐的内存上执行。 因此,将比例因子 16 用于 UWOP_SAVE_XMM128,允许小于 1M 的偏移。

展开操作代码是以下值之一:

  • UWOP_PUSH_NONVOL (0) 1 个节点

    压入非易失性整数寄存器,按 8 递减 RSP。 操作信息是寄存器的编号。 由于对 epilog 的约束,UWOP_PUSH_NONVOL 展开代码必须首先出现在 prolog 中,并相应地最后出现在展开代码数组中。 此相对排序适用于除 UWOP_PUSH_MACHFRAME 以外的其他所有展开代码。

  • UWOP_ALLOC_LARGE (1) 2 或 3 个节点

    在堆栈上分配大型区域。 有两种形式。 如果操作信息等于 0,则分配的大小除以 8 的值会记录在下一个槽中,允许分配最大为 512K - 8。 如果操作信息等于 1,则分配的无比例大小会采用 little-endian 格式记录在下两个槽中,允许分配最大为 4GB - 8。

  • UWOP_ALLOC_SMALL (2) 1 个节点

    在堆栈上分配小型区域。 分配的大小是操作信息字段 * 8 + 8,允许分配为 8 到 128 个字节。

    堆栈分配的展开代码应始终使用可能的最短编码:

    分配大小展开代码
    8 到 128 个字节 UWOP_ALLOC_SMALL
    136 到 512K-8 个字节 UWOP_ALLOC_LARGE = 0
    512K 到 4G-8 个字节 UWOP_ALLOC_LARGE操作信息 = 1
  • UWOP_SET_FPREG (3) 1 个节点

    通过将寄存器设置为当前 RSP 的某个偏移,来建立帧指针寄存器。 偏移等于 UNWIND_INFO 中的帧寄存器偏移(比例)字段 * 16,允许偏移为 0 到 240。 使用偏移可以建立指向固定堆栈分配中间的帧指针,允许更多访问使用短指令形式,从而帮助优化代码密度。 操作信息字段是保留字段,不应使用。

  • UWOP_SAVE_NONVOL (4) 2 个节点

    使用 MOV(而不是 PUSH)将非易失性整数寄存器保存在堆栈上。 此代码主要用于紧缩套装 ,其中非易失性寄存器会保存到堆栈中以前分配的位置。 操作信息是寄存器的编号。 以 8 为比例的堆栈偏移会记录在下一个展开操作代码槽中,如上面的备注中所述。

  • UWOP_SAVE_NONVOL_FAR (5) 3 个节点

    使用 MOV(而不是 PUSH),通过长偏移将非易失性整数寄存器保存在堆栈上。 此代码主要用于紧缩套装 ,其中非易失性寄存器会保存到堆栈中以前分配的位置。 操作信息是寄存器的编号。 无比例堆栈偏移会记录在下两个展开操作代码槽中,如上面的备注中所述。

  • UWOP_SAVE_XMM128 (8) 2 个节点

    将非易失性 XMM 寄存器的所有 128 位保存在堆栈上。 操作信息是寄存器的编号。 以 16 为比例的堆栈偏移会记录在下一个槽中。

  • UWOP_SAVE_XMM128_FAR (9) 3 个节点

    通过长偏移将非易失性 XMM 寄存器的所有 128 位保存在堆栈上。 操作信息是寄存器的编号。 无比例堆栈偏移会记录在下两个槽中。

  • UWOP_PUSH_MACHFRAME (10) 1 个节点

    压入计算机帧。 此展开代码用于记录硬件中断或异常的影响。 有两种形式。 如果操作信息等于 0,则其中一帧已压入堆栈:

    位置
    RSP+32 SS
    RSP+24 旧 RSP
    RSP+16 EFLAGS
    RSP+8 CS
    RSP RIP

    如果操作信息等于 1,则其中一帧已压入:

    位置
    RSP+40 SS
    RSP+32 旧 RSP
    RSP+24 EFLAGS
    RSP+16 CS
    RSP+8 RIP
    RSP 错误代码

    此展开代码始终出现在虚拟 prolog 中,后者实际上不会执行,而是出现在中断例程的实际入口点之前,它的存在只是为了提供一个位置来模拟计算机帧的压入。 UWOP_PUSH_MACHFRAME 会记录该模拟,这表示计算机已在概念上执行此操作:

    1. 弹出 RIP 会将堆栈顶部的地址返回到 Temp 中

    2. 压入 SS

    3. 压入旧 RSP

    4. 压入 EFLAGS

    5. 压入 CS

    6. 压入 Temp

    7. 压入错误代码(如果操作信息等于 1)

    模拟的 UWOP_PUSH_MACHFRAME 操作将 RSP 减 40(操作信息等于 0)或 48(操作信息等于 1)。

操作信息

操作信息位的含义取决于操作代码。 若要对常规用途(整数)寄存器进行编码,请使用此映射:

bit注册
0 RAX
1 RCX
2 RDX
3 RBX
4 RSP
5 RBP
6 RSI
7 RDI
8 到 15 R8 到 R15

链式展开信息结构

如果设置了 UNW_FLAG_CHAININFO 标志,则展开信息结构是辅助结构,共享异常处理程序/链接信息地址字段包含主展开信息。 此示例代码检索主展开信息,假设 unwindInfo 是设置了 UNW_FLAG_CHAININFO 标志的结构。

C++
PRUNTIME_FUNCTION primaryUwindInfo = (PRUNTIME_FUNCTION)&(unwindInfo->UnwindCode[( unwindInfo->CountOfCodes + 1 ) & ~1]);

链式信息在两种情况下十分有用。 首先,它可用于非连续代码段。 使用链式信息可以减少所需展开信息的大小,因为无需从主展开信息复制展开代码数组。

还可以使用链式信息对易失性寄存器保存进行分组。 编译器可能会延迟保存某些易失性寄存器,直到它位于函数入口 prolog 之外。 可以通过以下方式记录它们:让函数部分的主展开信息处于分组代码之前,然后使用非零大小的 prolog 设置链式信息(其中链式信息中的展开代码反映非易失性寄存器的保存)。 在这种情况下,展开代码是 UWOP_SAVE_NONVOL 的所有实例。 不支持使用 PUSH 保存非易失性寄存器或使用其他固定堆栈分配修改 RSP 寄存器的分组。

设置了 UNW_FLAG_CHAININFO 的 UNWIND_INFO 项可以包含其 UNWIND_INFO 项也设置了 UNW_FLAG_CHAININFO 的 RUNTIME_FUNCTION 条目,有时称为多个紧缩套装 。 最后,链式展开信息指针会到达清除了 UNW_FLAG_CHAININFO 的 UNWIND_INFO 项。 此项是主 UNWIND_INFO 项,它指向实际过程入口点。

展开过程

展开代码数组按降序排序。 发生异常时,操作系统会在上下文记录中存储完整上下文。 然后调用异常调度逻辑,这会重复执行以下步骤来查找异常处理程序:

  1. 使用上下文记录中存储的当前 RIP 搜索描述当前函数(或对于链式 UNWIND_INFO 条目为函数部分)的 RUNTIME_FUNCTION 表条目。

  2. 如果未找到任何函数表条目,则它位于叶函数中,并且 RSP 直接对返回指针寻址。 [RSP] 上的返回指针存储在更新后的上下文中,模拟的 RSP 按 8 递增,并且步骤 1 重复执行。

  3. 如果找到函数表条目,则 RIP 可能位于三个区域中:a) epilog 中,b) prolog 中,或 c) 可能由异常处理程序所涵盖的代码中。

    • 情况 a) 如果 RIP 在 epilog 中,则控制会离开函数,此函数可能没有与此异常关联的异常处理程序,epilog 的效果必须继续以计算调用方函数的上下文。 若要确定 RIP 是否在 epilog 中,请检查从 RIP 开始的代码流。 如果该代码流可与合法 epilog 的尾随部分匹配,则它在 epilog 中,epilog 的剩余部分会进行模拟,并在处理每个指令时更新上下文记录。 此处理完成之后,会重复步骤 1。

    • 情况 b) 如果 RIP 在 prologue 中,则控制未进入函数,此函数可能没有与此异常关联的异常处理程序,prolog 的效果必须撤消以计算调用方函数的上下文。 如果从函数开头到 RIP 的距离小于或等于在展开信息中编码的 prolog 大小,则 RIP 在 prolog 中。 prolog 的效果通过以下方式展开:在展开代码数组中向前扫描,查找偏移小于或等于 RIP 相对于函数开头的偏移的第一个条目,然后撤消展开代码数组中所有剩余项的效果。 然后重复步骤 1。

    • 情况 c) 如果 RIP 不在 prolog 或 epilog 中,并且函数具有异常处理程序(设置了 UNW_FLAG_EHANDLER),则调用特定于语言的处理程序。 处理程序会扫描其数据,并根据需要调用筛选器函数。 特定于语言的处理程序可以返回已处理异常或继续搜索。 它还可以直接启动展开。

  4. 如果特定于语言的处理程序返回已处理状态,则使用原始上下文记录继续执行。

  5. 如果没有特定于语言的处理程序或处理程序返回“继续搜索”状态,则必须将上下文记录展开为调用方的状态。 实现方法是处理所有展开代码数组元素,从而撤消每个元素的效果。 然后重复步骤 1。

涉及到链式展开信息时,仍然遵循这些基本步骤。 唯一的区别在于,当遍历展开代码数组以展开 prolog 的效果时,一旦到达数组末尾,它便会链接到父展开信息,并且遍历在其中找到的整个展开代码数组。 此链接将继续,直到到达没有 UNW_CHAINED_INFO 标志的展开信息,随后完成遍历其展开代码数组。

最小的展开数据集是 8 个字节。 这表示一个函数,该函数只分配了 128 个字节或更少的堆栈,并且可能保存了一个非易失性寄存器。 它也是不带展开代码的零长度 prolog 的链式展开信息结构大小。

特定于语言的处理程序

每当设置了标志 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER 时,特定于语言的处理程序的相对地址便会出现在 UNWIND_INFO 中。 如上一节所述,特定于语言的处理程序作为异常处理程序搜索的一部分或是展开的一部分进行调用。 它具有以下原型:

C++
typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN ULONG64 EstablisherFrame,
    IN OUT PCONTEXT ContextRecord,
    IN OUT PDISPATCHER_CONTEXT DispatcherContext
);

ExceptionRecord 提供指向具有标准 Win64 定义的异常记录的指针。

EstablisherFrame 是此函数的固定堆栈分配的基址。

ContextRecord 指向引发异常时的异常上下文(在异常处理程序情况下)或当前“展开”上下文(在终止处理程序情况下)。

DispatcherContext 指向此函数的调度程序上下文。 它具有以下定义:

C++
typedef struct _DISPATCHER_CONTEXT {
    ULONG64 ControlPc;
    ULONG64 ImageBase;
    PRUNTIME_FUNCTION FunctionEntry;
    ULONG64 EstablisherFrame;
    ULONG64 TargetIp;
    PCONTEXT ContextRecord;
    PEXCEPTION_ROUTINE LanguageHandler;
    PVOID HandlerData;
} DISPATCHER_CONTEXT, *PDISPATCHER_CONTEXT;

ControlPc 是此函数中的 RIP 值。 此值可以是异常地址,也可以是控制离开建立函数的位置处的地址。 RIP 用于确定控制是否在此函数内的某个受保护构造中,例如,__try/__except 或 __try/__finally 的 __try 块。

ImageBase 是包含此函数的模块的映像基(加载地址),要与函数入口和展开信息中使用的 32 位偏移相加,以记录相对地址。

FunctionEntry 提供指向 RUNTIME_FUNCTION 函数入口的指针,其中为此函数保存函数和展开信息映像基相对地址。

EstablisherFrame 是此函数的固定堆栈分配的基址。

TargetIp 提供指定展开的延续地址的可选指令地址。 如果未指定 EstablisherFrame ,则忽略此地址。

ContextRecord 指向异常上下文,供系统异常调度/展开代码使用。

LanguageHandler 指向所调用的特定于语言的语言处理程序例程。

HandlerData 指向此函数的特定于语言的处理程序数据。

MASM 的展开帮助程序

为了编写正确的程序集例程,有一组伪操作可与实际的程序集指令并行使用,以创建适当的 .pdata 和 .xdata。 并且有一组宏可简化伪操作的使用,以实现其最常见的用途。

原始伪操作

伪操作描述
PROC FRAME [:ehandler] 使 MASM 在 .pdata 中生成函数表条目,并在 .xdata 中生成展开信息,以实现函数的结构化异常处理展开行为。 如果 ehandler 存在,则此过程作为特定于语言的处理程序在 .xdata 中输入。

使用 FRAME 特性时,它必须后跟 .ENDPROLOG 指令。 如果函数是叶函数(如函数类型所定义),则不需要 FRAME 特性,其余的这些伪操作也是如此。
.PUSHREG register 使用序言中的当前偏移为指定寄存器编号生成 UWOP_PUSH_NONVOL 展开代码条目。

仅将它与非易失性整数寄存器一起使用。 对于易失性寄存器的压入,改为使用 .ALLOCSTACK 8
.SETFRAME register , offset 使用指定寄存器和偏移在展开信息中填充帧寄存器字段和偏移。 偏移必须是 16 的倍数,并且小于或等于 240。 此指令还使用当前序言偏移为指定寄存器生成 UWOP_SET_FPREG 展开代码条目。
.ALLOCSTACK size 针对序言中的当前偏移生成具有指定大小的 UWOP_ALLOC_SMALL 或 UWOP_ALLOC_LARGE。

size 操作数必须是 8 的倍数。
.SAVEREG register , offset 使用当前序言偏移为指定寄存器和偏移生成 UWOP_SAVE_NONVOL 或 UWOP_SAVE_NONVOL_FAR 展开代码条目。 MASM 会选择最高效的编码。

offset 必须为正,并且是 8 的倍数。 offset 相对于过程帧(通常在 RSP 中)的基址或无比例帧指针(如果使用帧指针)。
.SAVEXMM128 register, offset 使用当前序言偏移为指定 XMM 寄存器和偏移生成 UWOP_SAVE_XMM128 或 UWOP_SAVE_XMM128_FAR 展开代码条目。 MASM 会选择最高效的编码。

offset 必须为正,并且是 16 的倍数。 offset 相对于过程帧(通常在 RSP 中)的基址或无比例帧指针(如果使用帧指针)。
.PUSHFRAME [code] 生成 UWOP_PUSH_MACHFRAME 展开代码条目。 如果指定了可选的 code ,则为展开代码条目提供修饰符 1。 否则修饰符为 0。
.ENDPROLOG 指示序言声明的结尾。 必须出现在函数的前 255 个字节中。

下面是一个示例函数 prolog,其中包含大多数操作码的正确用法:

MASM
sample PROC FRAME
    db      048h; emit a REX prefix, to enable hot-patching
    push rbp
    .pushreg rbp
    sub rsp, 040h
    .allocstack 040h
    lea rbp, [rsp+020h]
    .setframe rbp, 020h
    movdqa [rbp], xmm7
    .savexmm128 xmm7, 020h ;the offset is from the base of the frame
                           ;not the scaled offset of the frame
    mov [rbp+018h], rsi
    .savereg rsi, 038h
    mov [rsp+010h], rdi
    .savereg rdi, 010h ; you can still use RSP as the base of the frame
                       ; or any other register you choose
    .endprolog

; you can modify the stack pointer outside of the prologue (similar to alloca)
; because we have a frame pointer.
; if we didn't have a frame pointer, this would be illegal
; if we didn't make this modification,
; there would be no need for a frame pointer

    sub rsp, 060h

; we can unwind from the next AV because of the frame pointer

    mov rax, 0
    mov rax, [rax] ; AV!

; restore the registers that weren't saved with a push
; this isn't part of the official epilog, as described in section 2.5

    movdqa xmm7, [rbp]
    mov rsi, [rbp+018h]
    mov rdi, [rbp-010h]

; Here's the official epilog

    lea rsp, [rbp+020h] ; deallocate both fixed and dynamic portions of the frame
    pop rbp
    ret
sample ENDP

有关 epilog 示例的详细信息,请参阅 x64 prolog 和 epilog 中的 Epilog 代码

MASM 宏

为了简化原始伪操作的使用,在 ksamd64.inc 中定义了一组宏,可用于创建典型过程序言和尾声。

描述
alloc_stack(n) 分配 n 个字节的堆栈帧(使用 sub rsp, n),并发出适当的展开信息 (.allocstack n)
save_reg reg, loc 将非易失性寄存器 reg 保存在堆栈上 RSP 偏移为 loc 的位置处,并发出适当的展开信息。 (.savereg reg, loc)
push_reg reg 将非易失性寄存器 reg 压入堆栈,并发出适当的展开信息。 (.pushreg reg)
rex_push_reg reg 使用 2 字节压入将非易失性寄存器保存在堆栈中,并发出适当的展开信息 (.pushreg reg)。 如果压入是函数中的第一个指令,则使用此宏,以确保函数是可进行热修补。
save_xmm128 reg, loc 将非易失性 XMM 寄存器 reg 保存在堆栈上 RSP 偏移为 loc 的位置处,并发出适当的展开信息 (.savexmm128 reg, loc)
set_frame reg, offset 将帧寄存器 reg 设置为 RSP + offset (使用 mov 或 lea),并发出适当的展开信息 (.set_frame reg, offset)
push_eflags 将 eflags 与 pushfq 指令一起压入,并发出适当的展开信息 (.alloc_stack 8)

下面是一个示例函数 prolog,其中包含宏的正确用法:

MASM
sampleFrame struct
    Fill     dq ?; fill to 8 mod 16
    SavedRdi dq ?; Saved Register RDI
    SavedRsi dq ?; Saved Register RSI
sampleFrame ends

sample2 PROC FRAME
    alloc_stack(sizeof sampleFrame)
    save_reg rdi, sampleFrame.SavedRdi
    save_reg rsi, sampleFrame.SavedRsi
    .end_prolog

; function body

    mov rsi, sampleFrame.SavedRsi[rsp]
    mov rdi, sampleFrame.SavedRdi[rsp]

; Here's the official epilog

    add rsp, (sizeof sampleFrame)
    ret
sample2 ENDP

展开数据定义(C 语言描述)

下面是展开数据的 C 语言描述:

C
typedef enum _UNWIND_OP_CODES {
    UWOP_PUSH_NONVOL = 0, /* info == register number */
    UWOP_ALLOC_LARGE,     /* no info, alloc size in next 2 slots */
    UWOP_ALLOC_SMALL,     /* info == size of allocation / 8 - 1 */
    UWOP_SET_FPREG,       /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */
    UWOP_SAVE_NONVOL,     /* info == register number, offset in next slot */
    UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */
    UWOP_SAVE_XMM128 = 8, /* info == XMM reg number, offset in next slot */
    UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */
    UWOP_PUSH_MACHFRAME   /* info == 0: no error-code, 1: error-code */
} UNWIND_CODE_OPS;

typedef unsigned char UBYTE;

typedef union _UNWIND_CODE {
    struct {
        UBYTE CodeOffset;
        UBYTE UnwindOp : 4;
        UBYTE OpInfo   : 4;
    };
    USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

#define UNW_FLAG_EHANDLER  0x01
#define UNW_FLAG_UHANDLER  0x02
#define UNW_FLAG_CHAININFO 0x04

typedef struct _UNWIND_INFO {
    UBYTE Version       : 3;
    UBYTE Flags         : 5;
    UBYTE SizeOfProlog;
    UBYTE CountOfCodes;
    UBYTE FrameRegister : 4;
    UBYTE FrameOffset   : 4;
    UNWIND_CODE UnwindCode[1];
/*  UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
*   union {
*       OPTIONAL ULONG ExceptionHandler;
*       OPTIONAL ULONG FunctionEntry;
*   };
*   OPTIONAL ULONG ExceptionData[]; */
} UNWIND_INFO, *PUNWIND_INFO;

typedef struct _RUNTIME_FUNCTION {
    ULONG BeginAddress;
    ULONG EndAddress;
    ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

#define GetUnwindCodeEntry(info, index) \
    ((info)->UnwindCode[index])

#define GetLanguageSpecificDataPtr(info) \
    ((PVOID)&GetUnwindCodeEntry((info),((info)->CountOfCodes + 1) & ~1))

#define GetExceptionHandler(base, info) \
    ((PEXCEPTION_HANDLER)((base) + *(PULONG)GetLanguageSpecificDataPtr(info)))

#define GetChainedFunctionEntry(base, info) \
    ((PRUNTIME_FUNCTION)((base) + *(PULONG)GetLanguageSpecificDataPtr(info)))

#define GetExceptionDataPtr(info) \
    ((PVOID)((PULONG)GetLanguageSpecificData(info) + 1))

请参阅

x64 软件约定

 
 

  • x64 prolog 和 epilog

    了解详细信息:x64 prolog 和 epilog

  • __security_init_cookie

    详细了解:__security_init_cookie

  • _AddressOfReturnAddress

    详细了解:_AddressOfReturnAddress

  • x64 调用约定

    了解有关默认 x64 调用约定的详细信息。

posted on 2023-02-04 16:05  不及格的程序员-八神  阅读(183)  评论(0编辑  收藏  举报