Skip to content

Commit de88423

Browse files
committed
crashdump: enable crashdump feature for release builds
- Improve documentation and describe how an user can create separate files for debug information - Change the output file directory to be configurable using HYPERLIGHT_CORE_DUMP_DIR environment variable - Change output file name to include a timestamp Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent 0e0a920 commit de88423

File tree

5 files changed

+186
-20
lines changed

5 files changed

+186
-20
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/how-to-debug-a-hyperlight-guest.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,149 @@ To do this in vscode, the following configuration can be used to add debug confi
274274
press the `pause` button. This is a known issue with the `CodeLldb` extension [#1245](https://github.com/vadimcn/codelldb/issues/1245).
275275
The `cppdbg` extension works as expected and stops at the entry point of the program.**
276276

277+
## Compiling guests with debug information for release builds
278+
279+
This section explains how to compile a guest with debugging information but still have optimized code, and how to separate the debug information from the binary.
280+
281+
### Creating a release build with debug information
282+
283+
To create a release build with debug information, you can add a custom profile to your `Cargo.toml` file:
284+
285+
```toml
286+
[profile.release-with-debug]
287+
inherits = "release"
288+
debug = true
289+
```
290+
291+
This creates a new profile called `release-with-debug` that inherits all settings from the release profile but adds debug information.
292+
293+
### Splitting debug information from the binary
294+
295+
To reduce the binary size while still having debug information available, you can split the debug information into a separate file.
296+
This is useful for production environments where you want smaller binaries but still want to be able to debug crashes.
297+
298+
Here's a step-by-step guide:
299+
300+
1. Build your guest with the release-with-debug profile:
301+
```bash
302+
cargo build --profile release-with-debug
303+
```
304+
305+
2. Locate your binary in the target directory:
306+
```bash
307+
TARGET_DIR="target"
308+
PROFILE="release-with-debug"
309+
ARCH="x86_64-unknown-none" # Your target architecture
310+
BUILD_DIR="${TARGET_DIR}/${ARCH}/${PROFILE}"
311+
BINARY=$(find "${BUILD_DIR}" -type f -executable -name "guest-binary" | head -1)
312+
```
313+
314+
3. Extract debug information into a full debug file:
315+
```bash
316+
DEBUG_FILE_FULL="${BINARY}.debug.full"
317+
objcopy --only-keep-debug "${BINARY}" "${DEBUG_FILE_FULL}"
318+
```
319+
320+
4. Create a symbols-only debug file (smaller, but still useful for stack traces):
321+
```bash
322+
DEBUG_FILE="${BINARY}.debug"
323+
objcopy --keep-file-symbols "${DEBUG_FILE_FULL}" "${DEBUG_FILE}"
324+
```
325+
326+
5. Strip debug information from the original binary but keep function names:
327+
```bash
328+
objcopy --strip-debug "${BINARY}"
329+
```
330+
331+
6. Add a debug link to the stripped binary:
332+
```bash
333+
objcopy --add-gnu-debuglink="${DEBUG_FILE}" "${BINARY}"
334+
```
335+
336+
After these steps, you'll have:
337+
- An optimized binary with function names for basic stack traces
338+
- A symbols-only debug file for stack traces
339+
- A full debug file for complete source-level debugging
340+
341+
### Analyzing core dumps with the debug files
342+
343+
When you have a core dump from a crashed guest, you can analyze it with different levels of detail using either GDB or LLDB.
344+
345+
#### Using GDB
346+
347+
1. For basic analysis with function names (stack traces):
348+
```bash
349+
gdb ${BINARY} -c /path/to/core.dump
350+
```
351+
352+
2. For full source-level debugging:
353+
```bash
354+
gdb -s ${DEBUG_FILE_FULL} ${BINARY} -c /path/to/core.dump
355+
```
356+
357+
#### Using LLDB
358+
359+
LLDB provides similar capabilities with slightly different commands:
360+
361+
1. For basic analysis with function names (stack traces):
362+
```bash
363+
lldb ${BINARY} -c /path/to/core.dump
364+
```
365+
366+
2. For full source-level debugging:
367+
```bash
368+
lldb -o "target create -c /path/to/core.dump ${BINARY}" -o "add-dsym ${DEBUG_FILE_FULL}"
369+
```
370+
371+
3. If your debug symbols are in a separate file:
372+
```bash
373+
lldb ${BINARY} -c /path/to/core.dump
374+
(lldb) add-dsym ${DEBUG_FILE_FULL}
375+
```
376+
377+
### VSCode Debug Configurations
378+
379+
You can configure VSCode (in `.vscode/launch.json`) to use these files by modifying the debug configurations:
380+
381+
#### For GDB
382+
383+
```json
384+
{
385+
"name": "[GDB] Load core dump with full debug symbols",
386+
"type": "cppdbg",
387+
"request": "launch",
388+
"program": "${input:program}",
389+
"coreDumpPath": "${input:core_dump}",
390+
"cwd": "${workspaceFolder}",
391+
"MIMode": "gdb",
392+
"externalConsole": false,
393+
"miDebuggerPath": "/usr/bin/gdb",
394+
"setupCommands": [
395+
{
396+
"description": "Enable pretty-printing for gdb",
397+
"text": "-enable-pretty-printing",
398+
"ignoreFailures": true
399+
}
400+
]
401+
}
402+
```
403+
404+
#### For LLDB
405+
406+
```json
407+
{
408+
"name": "[LLDB] Load core dump with full debug symbols",
409+
"type": "lldb",
410+
"request": "launch",
411+
"program": "${input:program}",
412+
"cwd": "${workspaceFolder}",
413+
"processCreateCommands": [],
414+
"targetCreateCommands": [
415+
"target create -c ${input:core_dump} ${input:program}"
416+
],
417+
"postRunCommands": [
418+
// if debug symbols are in a different file
419+
"add-dsym ${input:debug_file_path}"
420+
]
421+
}
422+
```

src/hyperlight_host/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ vmm-sys-util = "0.14.0"
3838
crossbeam = "0.8.0"
3939
crossbeam-channel = "0.5.15"
4040
thiserror = "2.0.12"
41-
tempfile = { version = "3.20", optional = true }
41+
chrono = { version = "0.4", optional = true }
4242
anyhow = "1.0"
4343
metrics = "0.24.2"
4444
serde_json = "1.0"
45-
elfcore = { git = "https://github.com/dblnz/elfcore.git", rev = "1a57f04272dd54bc06df638f4027debe6d0f694f" }
45+
elfcore = { git = "https://github.com/hyperlight-dev/elfcore.git", rev = "cef4c80e26bf4b2a5599e50d2d1730965f942c13" }
4646

4747
[target.'cfg(windows)'.dependencies]
4848
windows = { version = "0.61", features = [
@@ -124,7 +124,8 @@ function_call_metrics = []
124124
executable_heap = []
125125
# This feature enables printing of debug information to stdout in debug builds
126126
print_debug = []
127-
crashdump = ["dep:tempfile"] # Dumps the VM state to a file on unexpected errors or crashes. The path of the file will be printed on stdout and logged. This feature can only be used in debug builds.
127+
# Dumps the VM state to a file on unexpected errors or crashes. The path of the file will be printed on stdout and logged.
128+
crashdump = ["dep:chrono"]
128129
kvm = ["dep:kvm-bindings", "dep:kvm-ioctls"]
129130
mshv2 = ["dep:mshv-bindings2", "dep:mshv-ioctls2"]
130131
mshv3 = ["dep:mshv-bindings3", "dep:mshv-ioctls3"]

src/hyperlight_host/build.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ fn main() -> Result<()> {
9393
gdb: { all(feature = "gdb", debug_assertions, any(feature = "kvm", feature = "mshv2", feature = "mshv3"), target_os = "linux") },
9494
kvm: { all(feature = "kvm", target_os = "linux") },
9595
mshv: { all(any(feature = "mshv2", feature = "mshv3"), target_os = "linux") },
96-
// crashdump feature is aliased with debug_assertions to make it only available in debug-builds.
97-
crashdump: { all(feature = "crashdump", debug_assertions) },
96+
crashdump: { all(feature = "crashdump") },
9897
// print_debug feature is aliased with debug_assertions to make it only available in debug-builds.
9998
print_debug: { all(feature = "print_debug", debug_assertions) },
10099
// the following features are mutually exclusive but rather than enforcing that here we are enabling mshv3 to override mshv2 when both are enabled

src/hyperlight_host/src/hypervisor/crashdump.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ limitations under the License.
1616

1717
use std::cmp::min;
1818

19+
use chrono;
1920
use elfcore::{
2021
ArchComponentState, ArchState, CoreDumpBuilder, CoreError, Elf64_Auxv, ProcessInfoSource,
2122
ReadProcessMemory, ThreadView, VaProtection, VaRegion,
2223
};
23-
use tempfile::NamedTempFile;
2424

2525
use super::Hypervisor;
2626
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
@@ -103,12 +103,12 @@ impl GuestView {
103103
let filename = ctx
104104
.filename
105105
.as_ref()
106-
.map_or(|| "<unknown>".to_string(), |s| s.to_string());
106+
.map_or("<unknown>".to_string(), |s| s.to_string());
107107

108108
let cmd = ctx
109109
.binary
110110
.as_ref()
111-
.map_or(|| "<unknown>".to_string(), |s| s.to_string());
111+
.map_or("<unknown>".to_string(), |s| s.to_string());
112112

113113
// The xsave state is checked as it can be empty
114114
let mut components = vec![];
@@ -262,10 +262,6 @@ impl ReadProcessMemory for GuestMemReader {
262262
pub(crate) fn crashdump_to_tempfile(hv: &dyn Hypervisor) -> Result<()> {
263263
log::info!("Creating core dump file...");
264264

265-
// Create a temporary file with a recognizable prefix
266-
let temp_file = NamedTempFile::with_prefix("hl_core_")
267-
.map_err(|e| new_error!("Failed to create temporary file: {:?}", e))?;
268-
269265
// Get crash context from hypervisor
270266
let ctx = hv
271267
.crashdump_context()
@@ -278,16 +274,39 @@ pub(crate) fn crashdump_to_tempfile(hv: &dyn Hypervisor) -> Result<()> {
278274
// Create and write core dump
279275
let core_builder = CoreDumpBuilder::from_source(guest_view, memory_reader);
280276

277+
// Generate timestamp string for the filename using chrono
278+
let timestamp = chrono::Local::now()
279+
.format("%Y%m%d_T%H%M%S%.3f")
280+
.to_string();
281+
282+
// Determine the output directory based on environment variable
283+
let output_dir = if let Ok(dump_dir) = std::env::var("HYPERLIGHT_CORE_DUMP_DIR") {
284+
// Create the directory if it doesn't exist
285+
let path = std::path::Path::new(&dump_dir);
286+
if !path.exists() {
287+
std::fs::create_dir_all(path)
288+
.map_err(|e| new_error!("Failed to create core dump directory: {:?}", e))?;
289+
}
290+
std::path::PathBuf::from(dump_dir)
291+
} else {
292+
// Fall back to the system temp directory
293+
std::env::temp_dir()
294+
};
295+
296+
// Create the filename with timestamp
297+
let filename = format!("hl_core_{}.elf", timestamp);
298+
let file_path = output_dir.join(filename);
299+
300+
// Create the file
301+
let file = std::fs::File::create(&file_path)
302+
.map_err(|e| new_error!("Failed to create core dump file: {:?}", e))?;
303+
304+
// Write the core dump directly to the file
281305
core_builder
282-
.write(&temp_file)
306+
.write(&file)
283307
.map_err(|e| new_error!("Failed to write core dump: {:?}", e))?;
284308

285-
let persist_path = temp_file.path().with_extension("elf");
286-
temp_file
287-
.persist(&persist_path)
288-
.map_err(|e| new_error!("Failed to persist core dump file: {:?}", e))?;
289-
290-
let path_string = persist_path.to_string_lossy().to_string();
309+
let path_string = file_path.to_string_lossy().to_string();
291310

292311
println!("Core dump created successfully: {}", path_string);
293312
log::error!("Core dump file: {}", path_string);

0 commit comments

Comments
 (0)