線上服務存在難以追蹤的記憶體洩漏。需開發輕量級監控工具,支援兩種模式:
| 方案 | 優點 | 缺點 | 結論 |
|---|---|---|---|
重載 operator new/delete | 簡單直接 | 與 C 庫衝突、不攔截 malloc/free、std::string 等標準庫內部分配 | ❌ 放棄 |
#define malloc(x) my_malloc(x,__FILE__,__LINE__) | 易實現 | 無法攔截系統庫/標準庫內部 malloc(如 sprintf, new 底層) | ❌ 放棄 |
| 修改 glibc | 全攔截 | 影響整個系統、維護成本極高 | ❌ 放棄 |
__malloc_hook | GNU 擴展、標準機制 | 非執行緒安全、多執行緒需鎖、已棄用 (glibc 2.34+) | ❌ 放棄 |
| dlsym(RTLD_NEXT, "malloc") | 執行期動態攔截、不修改系統庫、執行緒相容、支援所有 malloc/free/realloc/calloc | 初始化順序需處理遞迴調用 | ✅ 採用 |
static void* (*sys_malloc)(size_t) = nullptr;
static void* (*sys_realloc)(void*, size_t) = nullptr;
static void* (*sys_calloc)(size_t, size_t) = nullptr;
static void (*sys_free)(void*) = nullptr;
static void initialize_functions() {
sys_malloc = reinterpret_cast(dlsym(RTLD_NEXT, "malloc"));
sys_realloc = reinterpret_cast(dlsym(RTLD_NEXT, "realloc"));
sys_calloc = reinterpret_cast(dlsym(RTLD_NEXT, "calloc"));
sys_free = reinterpret_cast(dlsym(RTLD_NEXT, "free"));
}
RTLD_NEXT 讓動態鏈接器在呼叫者之後的共享物件中尋找符號,即繞過自身 Hook 找到真正的 libc 實現。
dlsym 內部可能調用 calloc → 觸發 Hook → 死循環。解決:首次調用時使用靜態緩衝區滿足內部分配需求。
static char tmpbuf[1024];
static size_t tmppos = 0;
static bool is_initializing = false;
extern "C" void* malloc(size_t size) {
if (!sys_malloc) {
if (!is_initializing) {
is_initializing = true;
initialize_functions();
is_initializing = false;
} else {
// dlsym 內部分配:使用靜態緩衝區
void* ptr = tmpbuf + tmppos;
tmppos += (size + 7) & ~7; // 8-byte 對齊
return ptr;
}
}
// 正常路徑...
}
extern "C" void* malloc(size_t size) {
if (!sys_malloc) { /* 初始化邏輯 */ }
void* ptr = sys_malloc(size);
if (is_external && monitoring_enabled) {
record_allocation(ptr, size, backtrace());
}
return ptr;
}
extern "C" void free(void* ptr) {
if (!ptr) return;
if (is_external && monitoring_enabled) {
record_deallocation(ptr, backtrace());
}
sys_free(ptr);
}
is_external:__thread bool 標記,防止監控邏輯內部分配觸發遞迴 Hook。backtrace():獲取當前呼叫堆疊(backtrace(void** buffer, int size))。malloc_usable_size(ptr):獲取實際分配大小(含對齊填充)。
struct GeneralPerf {
int64_t used_memory;
vector stack_trace;
};
using GeneralPerfMap = unordered_map; // key = 位址
struct SpecificPerf {
size_t count = 0;
int64_t duration_ms = 0;
int64_t hold_memory = 0; // 監控期間淨增記憶體
int64_t allocated_memory = 0; // 監控期間總分配量
int64_t start_time = 0, end_time = 0;
GeneralPerfMap detail;
};
using SpecificPerfMap = unordered_map; // key = guard tag
struct ThreadPerfInfo {
int guards_num = 0;
GeneralPerfMap general_map;
vector guard_stack; // 嵌套 guard 標籤棧
vector specific_stack; // 嵌套專項監控棧
SpecificPerfMap specific_map; // 扁平化索引
~ThreadPerfInfo() { merge_to_global(); } // 執行緒退出自動合併
};
static __thread ThreadPerfInfo* tls_perf = nullptr;
#define PERFORMANCE_MONITOR(tag) \
PerfGuard _guard_##__LINE__(tag)
struct PerfGuard {
string tag;
int64_t start;
PerfGuard(const string& t) : tag(t), start(now_ms()) {
auto& info = get_tls_perf();
info.guards_num++;
info.guard_stack.push_back(tag);
info.specific_stack.emplace_back();
info.specific_stack.back().start_time = start;
}
~PerfGuard() {
auto& info = get_tls_perf();
auto& sp = info.specific_stack.back();
sp.end_time = now_ms();
sp.duration_ms = sp.end_time - sp.start_time;
info.specific_map[tag] = sp; // 扁平化存檔
if (info.specific_stack.size() > 1) {
// 合併到父層
info.specific_stack[info.specific_stack.size()-2].merge(sp);
}
info.specific_stack.pop_back();
info.guard_stack.pop_back();
info.guards_num--;
}
};
// 編譯為共享庫 libmemmonitor.so
// 啟動時預載入:LD_PRELOAD=./libmemmonitor.so ./my_server
// 程式碼中插桩專項監控
void suspicious_function() {
PERFORMANCE_MONITOR("suspicious_function");
// ... 疑似洩漏邏輯
}
// 定期或信號觸發報告生成
void generate_report() {
// 遍歷全域 GeneralPerfMap 找未釋放位址
// 遍歷 SpecificPerfMap 分析熱點區塊
}
基於 dlsym(RTLD_NEXT) 的 Hook 方案實現了非侵入式、執行緒安全、全攔截的記憶體監控。雙層監控架構(全域/專項)配合 TLS 無鎖記錄與 RAII Guard 介面,兼顧診斷完整性與生產環境低開銷需求。核心難點在於初始化遞迴與 Hook 遞迴防護,通過靜態緩衝區與 __thread 標記妥善解決。