windows 线程栈与异常

memetao 于 2024-07-24 发布

windows线程

线程包含三个东西:

内核对象:

alt text

Thread Context

Thread Stack

线程栈就是系统分配的一块内存,用来保存函数的局部变量、传给函数的变量

alt text

每个线程有2个栈, 一个在内核态,一个在用户态(https://www.cnblogs.com/5iedu/p/4888094.html)

用户态的栈空间默认是1MB。

内核态的栈常驻RAM(如果线程处于Running或者Redy状态),32位系统下是12KB,64位系统下是24KB。

Thread Environment Block (TEB)

TEB是一块系统分配的内存(用户态地址,可以被应用程序访问), 这块地址总共是1个PAGE(4KB x86|x64) alt text

2个特别重要的数据:

TEB的地址可以可以通过访问FS寄存器得到:

#include <intrin.h>
#include <winternl.h>

void *getTIB() {
#ifdef _M_IX86
    return (void *)__readfsdword(0x18);
#elif _M_AMD64
    return (void *)__readgsqword(0x30);
#else
#error unsupported architecture
#endif
}

线程栈的大小

有多种方式指定线程栈(用户态)空间的大小: 编译选项、链接指令、CreathreadEx参数 C++的 std::thread不支持修改。

模拟与观察栈溢出

#include <winternl.h>

// 先查看线程栈的基地址
void PrintThreadStackBaseAddress() {
    NT_TIB* ptr = (NT_TIB*)getTIB();
    std::printf("base address: %p", ptr->StackBase);
}

void StackOverFlow() {
    uint8_t buffer[1024 * 1024 - 15 * 1024] = {0};
    StackOverFlow();
}

int main() {
    PrintThreadStackBaseAddress();
    // GetThreadStackInformation();
    StackAddressOrder();
    StackOverFlow();
    int a;
    std::cin >> a;
    return a;
}

alt text

默认情况下栈大小是1MB,所以在最后的4KB(一页)上发生栈溢出(猜测)。

CRT中的栈地址检查函数

系统分配内存的时候并不会直接分配,而是等到发生缺页异常的时候(写某个地址)。那么久存在一个问题:

// sizeof(int) * 5KB 要分配整个空间么?
int a[1024 * 5] = {0};
a[0] = 1;

a[0] 处于栈的低地址(注意栈的增长方向)属于尚未分配的页, 为了解决这个问题, 编译器会插入栈检查函数,也就是说上面那个__chkstk()函数,这个函数的代码如下:

   102: _chkstk endp
   103:
   104:         end
   105:
00007FF7DC53302A  and         r10w,0F000h
   105:
00007FF7DC533030  lea         r11,[r11-1000h]
   105:
00007FF7DC533037  mov         byte ptr [r11],0
   105:
00007FF7DC53303B  cmp         r10,r11
   105:
00007FF7DC53303E  jne         cs10 (07FF7DC533030h)
   105:
00007FF7DC533040  mov         r10,qword ptr [rsp]
   105:
00007FF7DC533044  mov         r11,qword ptr [rsp+8]
   105:
00007FF7DC533049  add         rsp,10h

// 伪代码:

void StackCheck(int nBytesNeededFromStack)
{
    //获得栈顶指针,此时栈顶指针还没减去“局部变量”所示的空间大小
    PBYTE pbStackPtr = (CPU's stack pointer); //CPU栈顶指针
    while(nBytesNeededFromStack >= PAGESIZE)
    {
        //将栈顶指针移到PAGE_GUARD页面
        pbStackPtr -=PAGESIZE;

        //访问1个字节,以强迫系统调拨下一个页面
        pbStackPtr[0] = 0;

        //剩下需要调拨的字节数
        nBytesNeededFromStack -= PAGESIZE;
    }
    //用返回之前,StatckCheck函数将CPU的栈顶指针设置在调用函数
    //的局部变量下
}

检查代码的原理很简单:每次试图访问下一个页面中的某个地址,以使系统自动为它分配调拨内存,直到需要的栈空间都满足为止。当然如果预设的栈空间不够的话,还是会先引发溢出异常。

结构化异常(SEH)

异常链: SEH 维护了一个线程私有的异常链,保存在 TEB 的 ExceptionList 中(TEB->NtTib.ExceptionList)。

typedef struct _NT_TIB {
    struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
    // ...
} NT_TIB;

typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_DISPOSITION (*Handler)(   // 函数指针
        struct _EXCEPTION_RECORD *ExceptionRecord,
        void *EstablisherFrame,
        struct _CONTEXT *ContextRecord,
        void *DispatcherContext
    );
} EXCEPTION_REGISTRATION_RECORD;

Windows 的异常调度流程:

C++ 异常

C++ 异常是基于栈展开(stack unwinding)的。流程如下:

try {
    funcA();
} catch (const std::exception& e) {
    // 处理异常
}

当 funcA() 抛出异常时:

MSVC 编译器默认将 C++ 异常构建在 SEH 之上,具体来说:

构件 说明
throw 调用 _CxxThrowException()(在 vcruntime 中)
_CxxFrameHandler3 每个函数的 EH 信息处理器(在异常时被 SEH 调用)
.pdata/.xdata 每个函数的异常处理元数据,用于匹配 catch

_CxxFrameHandler3 由 Windows SEH 调用,在发生异常时负责:

问题 解答
C++ 异常如何传递? 通过 _CxxThrowException 抛出,沿调用栈查找匹配的 catch,过程中析构对象
抛出时会泄露资源吗? 不会,符合 RAII 的资源都会被调用析构函数释放(但裸指针资源会泄露)
异常能跨线程吗? 不可以,异常不能越过线程边界,throw 出去必须在同一线程 catch
SEH 能 catch C++ 异常吗? 不行,__except 无法捕获 throw 抛出的异常,反之亦然
C++ 异常一定会崩溃程序吗? 不一定,未捕获异常会调用 std::terminate(),你可以自定义 terminate_handler

一些进阶特性: | 特性 | 说明 | | ———————— | ————————————————————————————————————- | | noexcept | 标记函数不可抛出异常,编译器会插入崩溃代码(std::terminate()) | | std::exception_ptr | 异常对象跨线程传递方式 | | std::rethrow_exception | 延迟处理异常 | | 异常匹配顺序 | 从上往下匹配,支持类型兼容性,如 catch(std::runtime_error&) 也能匹配 throw std::logic_error()(因为继承) |

敲重点: SEH不能捕获C++异常

来源不同: | 源头 | 属于哪类异常 | | ———————————– | ———— | | throw | C++ 异常 | | 除 0 / 空指针 | SEH 异常 | | WinAPI 抛出异常(RaiseException) | SEH 异常 |

是否析构对象: | 异常类型 | 栈展开时析构对象? | | ——– | ————————- | | C++ 异常 | ✅ 是,标准 RAII 流程 | | SEH 异常 | ❌ 否,不调用 C++ 析构函数 |