Skip to content

Commit a33ce64

Browse files
committed
feat: crash dump + 栈回溯
1 parent c5775b1 commit a33ce64

5 files changed

Lines changed: 283 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ windows-sys = { version = "0.59", features = [
1616
"Win32_System_Memory",
1717
"Win32_System_SystemInformation",
1818
"Win32_System_IO",
19+
"Win32_System_Diagnostics_Debug",
20+
"Win32_System_Kernel",
21+
"Win32_System_ProcessStatus",
1922
"Win32_Storage_FileSystem",
2023
] }
2124
retour = "0.4.0-alpha.4"

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ unsafe extern "system" fn DllMain(h_module: HMODULE, reason: u32, _reserved: *mu
3535
match reason {
3636
DLL_PROCESS_ATTACH => {
3737
DisableThreadLibraryCalls(h_module);
38+
loader::crash_dump::install();
3839
CreateThread(
3940
std::ptr::null(),
4041
0,

src/loader/crash_dump.rs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
use std::ffi::c_void;
2+
use std::fs::{self, File};
3+
use std::io::Write;
4+
use std::mem::{size_of, zeroed};
5+
use std::ptr::{null, null_mut};
6+
7+
use windows_sys::Win32::Foundation::{BOOL, HANDLE, MAX_PATH};
8+
use windows_sys::Win32::Storage::FileSystem::CREATE_ALWAYS;
9+
use windows_sys::Win32::System::Diagnostics::Debug::{
10+
AddrModeFlat, MiniDumpNormal, MiniDumpWriteDump, StackWalk, SymCleanup, SymFromAddr,
11+
SymFunctionTableAccess, SymGetModuleBase, SymInitialize, EXCEPTION_POINTERS,
12+
MINIDUMP_EXCEPTION_INFORMATION, STACKFRAME, SYMBOL_INFO,
13+
};
14+
use windows_sys::Win32::System::ProcessStatus::{GetModuleBaseNameA, GetModuleInformation, MODULEINFO};
15+
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetCurrentProcessId, GetCurrentThread, GetCurrentThreadId};
16+
17+
use super::log::{log_error, log_info};
18+
use super::pe::get_self_base_dir;
19+
20+
const EXCEPTION_EXECUTE_HANDLER: i32 = 1;
21+
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
22+
const GENERIC_WRITE: u32 = 0x40000000;
23+
const IMAGE_FILE_MACHINE_I386: u32 = 0x014c;
24+
25+
extern "system" {
26+
fn CreateDirectoryA(path: *const u8, security: *const c_void) -> i32;
27+
fn CreateFileA(
28+
name: *const u8,
29+
access: u32,
30+
share: u32,
31+
security: *const c_void,
32+
disposition: u32,
33+
flags: u32,
34+
template: *mut c_void,
35+
) -> HANDLE;
36+
fn CloseHandle(handle: HANDLE) -> BOOL;
37+
fn GetLocalTime(st: *mut SYSTEMTIME);
38+
fn SetUnhandledExceptionFilter(
39+
filter: Option<unsafe extern "system" fn(*mut EXCEPTION_POINTERS) -> i32>,
40+
) -> Option<unsafe extern "system" fn(*mut EXCEPTION_POINTERS) -> i32>;
41+
}
42+
43+
#[repr(C)]
44+
struct SYSTEMTIME {
45+
w_year: u16,
46+
w_month: u16,
47+
w_day_of_week: u16,
48+
w_day: u16,
49+
w_hour: u16,
50+
w_minute: u16,
51+
w_second: u16,
52+
w_milliseconds: u16,
53+
}
54+
55+
#[cfg(target_arch = "x86")]
56+
type NativeContext = windows_sys::Win32::System::Diagnostics::Debug::CONTEXT;
57+
58+
pub unsafe fn install() {
59+
SetUnhandledExceptionFilter(Some(unhandled_exception_filter));
60+
log_info("crash dump handler installed");
61+
}
62+
63+
unsafe extern "system" fn unhandled_exception_filter(exception: *mut EXCEPTION_POINTERS) -> i32 {
64+
if let Err(err) = write_crash_report(exception) {
65+
log_error(&format!("failed to write crash dump: {}", err));
66+
}
67+
EXCEPTION_EXECUTE_HANDLER
68+
}
69+
70+
unsafe fn write_crash_report(exception: *mut EXCEPTION_POINTERS) -> Result<(), String> {
71+
let base_dir = get_self_base_dir().ok_or_else(|| "cannot resolve base dir".to_string())?;
72+
let crash_dir = format!("{}\\mods\\crash", base_dir);
73+
CreateDirectoryA(format!("{}\0", format!("{}\\mods", base_dir)).as_ptr(), null());
74+
CreateDirectoryA(format!("{}\0", crash_dir).as_ptr(), null());
75+
76+
let stamp = timestamp();
77+
let dump_path = format!("{}\\crash_{}.dmp", crash_dir, stamp);
78+
let log_path = format!("{}\\crash_{}.log", crash_dir, stamp);
79+
80+
write_minidump(&dump_path, exception)?;
81+
write_text_log(&log_path, exception, &dump_path)?;
82+
log_error(&format!("crash dump written: {}", dump_path));
83+
Ok(())
84+
}
85+
86+
unsafe fn write_minidump(path: &str, exception: *mut EXCEPTION_POINTERS) -> Result<(), String> {
87+
let path_c = format!("{}\0", path);
88+
let file = CreateFileA(
89+
path_c.as_ptr(),
90+
GENERIC_WRITE,
91+
0,
92+
null(),
93+
CREATE_ALWAYS,
94+
FILE_ATTRIBUTE_NORMAL,
95+
null_mut(),
96+
);
97+
if file == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
98+
return Err(format!("CreateFileA failed for {}", path));
99+
}
100+
101+
let mut exception_info = MINIDUMP_EXCEPTION_INFORMATION {
102+
ThreadId: GetCurrentThreadId(),
103+
ExceptionPointers: exception,
104+
ClientPointers: 0,
105+
};
106+
let ok = MiniDumpWriteDump(
107+
GetCurrentProcess(),
108+
GetCurrentProcessId(),
109+
file,
110+
MiniDumpNormal,
111+
&mut exception_info,
112+
null_mut(),
113+
null_mut(),
114+
);
115+
CloseHandle(file);
116+
if ok == 0 {
117+
return Err("MiniDumpWriteDump failed".to_string());
118+
}
119+
Ok(())
120+
}
121+
122+
unsafe fn write_text_log(path: &str, exception: *mut EXCEPTION_POINTERS, dump_path: &str) -> Result<(), String> {
123+
let mut file = File::create(path).map_err(|err| err.to_string())?;
124+
writeln!(file, "ChuModLoader crash report").map_err(|err| err.to_string())?;
125+
writeln!(file, "dump: {}", dump_path).map_err(|err| err.to_string())?;
126+
127+
if exception.is_null() || (*exception).ExceptionRecord.is_null() {
128+
writeln!(file, "exception: <null>").map_err(|err| err.to_string())?;
129+
return Ok(());
130+
}
131+
132+
let record = &*(*exception).ExceptionRecord;
133+
writeln!(file, "exception_code: 0x{:08X}", record.ExceptionCode).map_err(|err| err.to_string())?;
134+
writeln!(file, "exception_address: 0x{:08X}", record.ExceptionAddress as usize).map_err(|err| err.to_string())?;
135+
if let Some(module) = module_offset(record.ExceptionAddress as usize) {
136+
writeln!(file, "exception_module: {}+0x{:X}", module.0, module.1).map_err(|err| err.to_string())?;
137+
}
138+
139+
#[cfg(target_arch = "x86")]
140+
if !(*exception).ContextRecord.is_null() {
141+
write_registers(&mut file, &*((*exception).ContextRecord as *const NativeContext))?;
142+
write_stack_trace(&mut file, &mut *((*exception).ContextRecord as *mut NativeContext))?;
143+
}
144+
145+
#[cfg(not(target_arch = "x86"))]
146+
writeln!(file, "registers/stack: unsupported target arch").map_err(|err| err.to_string())?;
147+
148+
Ok(())
149+
}
150+
151+
#[cfg(target_arch = "x86")]
152+
fn write_registers(file: &mut File, ctx: &NativeContext) -> Result<(), String> {
153+
writeln!(file, "registers:").map_err(|err| err.to_string())?;
154+
writeln!(file, " EAX=0x{:08X} EBX=0x{:08X} ECX=0x{:08X} EDX=0x{:08X}", ctx.Eax, ctx.Ebx, ctx.Ecx, ctx.Edx).map_err(|err| err.to_string())?;
155+
writeln!(file, " ESI=0x{:08X} EDI=0x{:08X} EBP=0x{:08X} ESP=0x{:08X}", ctx.Esi, ctx.Edi, ctx.Ebp, ctx.Esp).map_err(|err| err.to_string())?;
156+
writeln!(file, " EIP=0x{:08X} EFLAGS=0x{:08X}", ctx.Eip, ctx.EFlags).map_err(|err| err.to_string())?;
157+
Ok(())
158+
}
159+
160+
#[cfg(target_arch = "x86")]
161+
unsafe fn write_stack_trace(file: &mut File, ctx: &mut NativeContext) -> Result<(), String> {
162+
writeln!(file, "stack_trace:").map_err(|err| err.to_string())?;
163+
let process = GetCurrentProcess();
164+
let thread = GetCurrentThread();
165+
SymInitialize(process, null(), 1);
166+
167+
let mut frame: STACKFRAME = zeroed();
168+
frame.AddrPC.Offset = ctx.Eip;
169+
frame.AddrPC.Mode = AddrModeFlat;
170+
frame.AddrFrame.Offset = ctx.Ebp;
171+
frame.AddrFrame.Mode = AddrModeFlat;
172+
frame.AddrStack.Offset = ctx.Esp;
173+
frame.AddrStack.Mode = AddrModeFlat;
174+
175+
for index in 0..64 {
176+
let ok = StackWalk(
177+
IMAGE_FILE_MACHINE_I386,
178+
process,
179+
thread,
180+
&mut frame,
181+
ctx as *mut _ as *mut c_void,
182+
None,
183+
Some(SymFunctionTableAccess),
184+
Some(SymGetModuleBase),
185+
None,
186+
);
187+
if ok == 0 || frame.AddrPC.Offset == 0 {
188+
break;
189+
}
190+
let addr = frame.AddrPC.Offset;
191+
let symbol = symbol_from_addr(process, addr as u64).unwrap_or_else(|| module_offset(addr as usize).map(|(m, o)| format!("{}+0x{:X}", m, o)).unwrap_or_else(|| "<unknown>".to_string()));
192+
writeln!(file, " #{:02} 0x{:08X} {}", index, addr as u32, symbol).map_err(|err| err.to_string())?;
193+
}
194+
195+
SymCleanup(process);
196+
Ok(())
197+
}
198+
199+
#[cfg(target_arch = "x86")]
200+
unsafe fn symbol_from_addr(process: HANDLE, addr: u64) -> Option<String> {
201+
let mut storage = [0u8; size_of::<SYMBOL_INFO>() + 512];
202+
let symbol = storage.as_mut_ptr() as *mut SYMBOL_INFO;
203+
(*symbol).SizeOfStruct = size_of::<SYMBOL_INFO>() as u32;
204+
(*symbol).MaxNameLen = 511;
205+
let mut displacement = 0u64;
206+
if SymFromAddr(process, addr, &mut displacement, symbol) == 0 {
207+
return None;
208+
}
209+
let name_ptr = (*symbol).Name.as_ptr() as *const u8;
210+
let len = (0..511).position(|i| *name_ptr.add(i) == 0).unwrap_or(511);
211+
let name = String::from_utf8_lossy(std::slice::from_raw_parts(name_ptr, len)).into_owned();
212+
Some(format!("{}+0x{:X}", name, displacement))
213+
}
214+
215+
unsafe fn module_offset(addr: usize) -> Option<(String, usize)> {
216+
let module = module_from_address(addr)?;
217+
let mut name = [0u8; MAX_PATH as usize];
218+
let len = GetModuleBaseNameA(
219+
GetCurrentProcess(),
220+
module,
221+
name.as_mut_ptr(),
222+
name.len() as u32,
223+
);
224+
let module_name = if len == 0 {
225+
"<module>".to_string()
226+
} else {
227+
String::from_utf8_lossy(&name[..len as usize]).into_owned()
228+
};
229+
Some((module_name, addr.saturating_sub(module as usize)))
230+
}
231+
232+
unsafe fn module_from_address(addr: usize) -> Option<*mut c_void> {
233+
let mut module = null_mut();
234+
let flags = 0x00000004u32 | 0x00000002u32;
235+
let ok = windows_sys::Win32::System::LibraryLoader::GetModuleHandleExA(
236+
flags,
237+
addr as *const u8,
238+
&mut module,
239+
);
240+
if ok == 0 || module.is_null() {
241+
return None;
242+
}
243+
let mut info: MODULEINFO = zeroed();
244+
if GetModuleInformation(GetCurrentProcess(), module, &mut info, size_of::<MODULEINFO>() as u32) == 0 {
245+
return None;
246+
}
247+
Some(module)
248+
}
249+
250+
unsafe fn timestamp() -> String {
251+
let mut st: SYSTEMTIME = zeroed();
252+
GetLocalTime(&mut st);
253+
format!(
254+
"{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}",
255+
st.w_year, st.w_month, st.w_day, st.w_hour, st.w_minute, st.w_second, st.w_milliseconds
256+
)
257+
}
258+
259+
pub fn log_panic_context(scope: &str, name: &str) {
260+
let base_dir = get_self_base_dir().unwrap_or_default();
261+
if base_dir.is_empty() {
262+
log_error(&format!("panic caught in {}: {}", scope, name));
263+
return;
264+
}
265+
let crash_dir = format!("{}\\mods\\crash", base_dir);
266+
let _ = fs::create_dir_all(&crash_dir);
267+
let path = format!("{}\\panic_{}.log", crash_dir, unsafe { timestamp() });
268+
if let Ok(mut file) = File::create(&path) {
269+
let _ = writeln!(file, "ChuModLoader panic context");
270+
let _ = writeln!(file, "scope: {}", scope);
271+
let _ = writeln!(file, "mod: {}", name);
272+
}
273+
log_error(&format!("panic caught in {}: {} (context={})", scope, name, path));
274+
}

src/loader/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod dependency;
2+
pub mod crash_dump;
23
pub mod frame_hook;
34
pub mod hot_reload;
45
pub mod log;

src/loader/seh.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub unsafe fn call_mod_init(
1515
match catch_unwind(AssertUnwindSafe(|| init(info, api))) {
1616
Ok(ret) => Some(ret),
1717
Err(_) => {
18+
super::crash_dump::log_panic_context("chumod_init", name);
1819
log_info(&format!("mod init panic caught, skip mod: {}", name));
1920
None
2021
}
@@ -23,12 +24,14 @@ pub unsafe fn call_mod_init(
2324

2425
pub unsafe fn call_mod_shutdown(name: &str, shutdown: ChuModShutdownFunc) {
2526
if catch_unwind(AssertUnwindSafe(|| shutdown())).is_err() {
27+
super::crash_dump::log_panic_context("chumod_shutdown", name);
2628
log_info(&format!("mod shutdown panic caught: {}", name));
2729
}
2830
}
2931

3032
pub unsafe fn call_mod_on_ready(name: &str, on_ready: ChuModReadyFunc) {
3133
if catch_unwind(AssertUnwindSafe(|| on_ready())).is_err() {
34+
super::crash_dump::log_panic_context("chumod_on_ready", name);
3235
log_info(&format!(
3336
"mod on_ready panic caught: {} (callback=chumod_on_ready)",
3437
name
@@ -38,6 +41,7 @@ pub unsafe fn call_mod_on_ready(name: &str, on_ready: ChuModReadyFunc) {
3841

3942
pub unsafe fn call_mod_on_frame(name: &str, on_frame: ChuModFrameFunc) {
4043
if catch_unwind(AssertUnwindSafe(|| on_frame())).is_err() {
44+
super::crash_dump::log_panic_context("chumod_on_frame", name);
4145
log_info(&format!(
4246
"mod on_frame panic caught: {} (callback=chumod_on_frame)",
4347
name

0 commit comments

Comments
 (0)