Theo Dõi Việc Gọi Đến Các Phương Thức Của Đối Tượng Singleton - nói dối e blog

Theo Dõi Việc Gọi Đến Các Phương Thức Của Đối Tượng Singleton

Trong động cơ game hiện tại của chúng ta, toàn bộ các đối tượng Singleton đều được quản lý thông qua một lớp quản lý trung tâm. Bất kỳ module nào muốn truy cập một Singleton đều phải thông qua phương thức thống nhất do lớp quản lý cung cấp.

Trong quá trình gỡ lỗi, tôi gặp phải một yêu cầu đặc biệt: cần chương trình tự động dừng thực thi và chuyển sang chế độ debug mỗi khi một phương thức của Singleton bị gọi.

Về lý thuyết, nếu mọi module đều sử dụng phương thức get_instance() để lấy đối tượng Singleton trước khi gọi các hàm của nó, chúng ta chỉ cần đặt điểm dừng tại get_instance() là đủ. Tuy nhiên, trong hệ thống của chúng ta, các module đã lưu trữ con trỏ Singleton ngay từ giai đoạn khởi tạo vào các biến toàn cục. Sau đó, mọi thao tác với Singleton đều sử dụng trực tiếp con trỏ này, khiến việc theo dõi trở nên bất khả thi.

Để giải quyết vấn đề này, tôi đã áp dụng một kỹ thuật “hack” thông qua cơ chế bảng hàm ảo (virtual table) như sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <cstdio>
#include <cstdlib>
#include <cstring>

// Giao diện cơ sở với 2 phương thức ảo
struct i_foobar { 
    virtual void foo(int arg)=0; 
    virtual void bar()=0; 
};

// Triển khai cụ thể
class foobar : public i_foobar { 
public:
    virtual void foo(int arg) { printf("%d",arg); }
    virtual void bar() { printf("bar"); }
};

// Cấu trúc mã giả lập cho proxy
#pragma pack(push,1)
struct __proxy_code { 
    unsigned char mov_edx;   // MOV EDX, index
    unsigned long index;     // Chỉ số phương thức
    unsigned char jmp;       // JMP __proxy_gate
    unsigned long offset;    // Độ lệch đến cổng xử lý
    unsigned char nop1;      // Padding
    unsigned char nop2;      // Padding
};
#pragma pack(pop)

// Bảng ảo giả lập và vùng mã chuyển tiếp
static const void* __proxy_virtual_table[64]; 
static __proxy_code __proxy_bridge_code[64]; 

// Cổng xử lý trung tâm - chèn điểm dừng
__declspec(naked) void __proxy_gate() { 
    __asm { 
        int 3            // Điểm dừng debugger
        mov ecx,[ecx-4]  // Lấy địa chỉ instance thật
        mov eax,[ecx]    // Trỏ đến bảng ảo giả
        add eax,edx      // Tính vị trí phương thức
        mov eax,[eax]    // Lấy địa chỉ gốc
        jmp eax          // Chuyển đến hàm thật
    } 
}

// Đối tượng proxy chứa thông tin chuyển tiếp
struct __proxy_t { 
    void *__instance;   // Con trỏ đến đối tượng thật
    void *__vtbl;       // Bảng ảo giả lập
}; 

// Khởi tạo vùng mã giả lập
void init_proxy() { 
    for (int i=0; i<64; i++) {
        // Cấu hình mã máy cho mỗi phương thức:
        __proxy_bridge_code[i].mov_edx = 0xBA;   // Opcode MOV EDX
        __proxy_bridge_code[i].index = i;        // Chỉ số phương thức
        __proxy_bridge_code[i].jmp = 0xE9;       // Opcode JMP
        __proxy_bridge_code[i].offset = (unsigned long)__proxy_gate - (unsigned long)&__proxy_bridge_code[i] - 5;
        __proxy_bridge_code[i].nop1 = 0x90;      // NOP padding
        __proxy_bridge_code[i].nop2 = 0x90;
        
        // Cập nhật bảng ảo giả
        __proxy_virtual_table[i] = &__proxy_bridge_code[i];
    }
} 

// Tạo proxy bọc quanh đối tượng thật
template<typename T>
T* create_proxy(T* instance) { 
    __proxy_t *p = new __proxy_t;
    p->__instance = instance;
    p->__vtbl = __proxy_virtual_table;
    return (T*)&(p->__vtbl);
} 

// Ví dụ sử dụng
int main() { 
    init_proxy();
    
    // Tạo đối tượng thật
    i_foobar *foo = new foobar; 
    
    // Tạo proxy theo dõi mọi phương thức
    i_foobar *proxy = create_proxy(foo); 
    
    // Gọi phương thức - chương trình sẽ dừng tại int 3!
    proxy->foo(100); 
    proxy->bar(); 
    
    return 0;
} 

Giải thích chi tiết:

  1. Cơ chế hoạt động:

    • Chúng ta tạo ra một “bảng hàm ảo giả” (__proxy_virtual_table) thay thế bảng hàm ảo gốc của đối tượng Singleton.
    • Mỗi mục trong bảng này trỏ đến một đoạn mã máy đặc biệt (__proxy_bridge_code) chứa lệnh int 3 để kích hoạt debugger.
    • Khi một phương thức bị gọi, luồng thực thi sẽ đi qua cổng __proxy_gate, cho phép chúng ta can thiệp trước khi chuyển đến hàm gốc.
  2. Tính linh hoạt:

    • Kỹ thuật này không giới hạn ở i_foobar - nó có thể áp dụng cho bất kỳ giao diện nào có tối đa 64 phương thức ảo.
    • Thay vì chỉ dừng chương trình, bạn có thể mở rộng để ghi log, đo thời gian thực thi, hoặc thậm chí thay đổi tham số đầu vào/đầu ra.
  3. Hạn chế cần lưu ý:

    • Ví dụ trên thiếu cơ chế giải phóng bộ nhớ (memory leak) - chỉ dùng để minh họa.
    • Phụ thuộc vào kiến trúc x86 và trình biên dịch Microsoft Visual C++.
    • Cần disable các kiểm tra bảo vệ mã máy (DEP/NX bit) để vùng nhớ __proxy_bridge_code có thể thực thi.

Ứng dụng thực tế:

  • Theo dõi lỗi đồng thời (concurrency bugs) khi nhiều thread truy cập Singleton.
  • Phân tích hành vi của các module hệ thống mà không cần sửa đổi mã nguồn gốc.
  • Xây dựng framework ghi log hành vi runtime cho mục đích kiểm thử.
0%