This commit is contained in:
illustris
2026-01-08 18:11:30 +05:30
commit 4fb1bd90db
32 changed files with 3058 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
CC = gcc
CFLAGS_BASE = -O2 -Wall
all: nodebug withdebug
nodebug: program.c
$(CC) $(CFLAGS_BASE) -o $@ $< -lm
withdebug: program.c
$(CC) $(CFLAGS_BASE) -g -o $@ $< -lm
# Show the size difference
sizes: nodebug withdebug
@echo "File sizes:"
@ls -lh nodebug withdebug
@echo ""
@echo "Section sizes (nodebug):"
@size nodebug
@echo ""
@echo "Section sizes (withdebug):"
@size withdebug
# Show symbols
symbols: nodebug withdebug
@echo "Symbols in nodebug:"
@nm nodebug | grep -E "compute|process|main" || true
@echo ""
@echo "Symbols in withdebug:"
@nm withdebug | grep -E "compute|process|main" || true
# Strip the debug version to show what happens
stripped: withdebug
cp withdebug stripped
strip stripped
@echo "Stripped file created"
clean:
rm -f nodebug withdebug stripped perf.data perf.data.old
.PHONY: all sizes symbols clean stripped

View File

@@ -0,0 +1,179 @@
# Scenario 5: Debug Symbols - The Missing Map
## Learning Objectives
- Understand what debug symbols are and why they matter
- Compare profiling output with and without symbols
- Use `perf annotate` to see source-level hotspots
- Understand the trade-offs of shipping debug symbols
## Background
When you compile C code, the compiler translates your source into machine code.
By default, the connection between source lines and machine instructions is lost.
**Debug symbols** (enabled with `-g`) preserve this mapping:
- Function names
- Source file names and line numbers
- Variable names and types
- Inline function information
## Files
- `program.c` - A program with nested function calls
- `Makefile` - Builds `nodebug` and `withdebug` versions
## Exercise 1: Build Both Versions
```bash
make all
```
Compare file sizes:
```bash
make sizes
```
The `withdebug` binary is larger due to DWARF debug sections.
## Exercise 2: Profile Without Debug Symbols
```bash
perf record ./nodebug 500 5000
perf report
```
You'll see something like:
```
45.23% nodebug nodebug [.] 0x0000000000001234
32.11% nodebug nodebug [.] 0x0000000000001456
12.45% nodebug libm.so [.] __sin_fma
```
The hex addresses (`0x...`) tell you nothing useful!
## Exercise 3: Profile With Debug Symbols
```bash
perf record ./withdebug 500 5000
perf report
```
Now you see:
```
45.23% withdebug withdebug [.] compute_inner
32.11% withdebug withdebug [.] compute_middle
12.45% withdebug libm.so [.] __sin_fma
```
Much better! You can see which functions are hot.
## Exercise 4: Source-Level Annotation
With debug symbols, you can see exactly which lines are hot:
```bash
perf record ./withdebug 500 5000
perf annotate compute_inner
```
This shows the source code with cycle counts per line!
```
│ double compute_inner(double x, int iterations) {
│ double result = x;
1.23 │ for (int i = 0; i < iterations; i++) {
45.67 │ result = sin(result) * cos(result) + sqrt(fabs(result));
23.45 │ result = result * 1.0001 + 0.0001;
│ }
│ return result;
│ }
```
## Exercise 5: Understanding Symbol Tables
```bash
# Show symbols in each binary
make symbols
# Or manually:
nm nodebug | head -20
nm withdebug | head -20
# Show more detail about sections
readelf -S withdebug | grep debug
```
Debug sections you might see:
- `.debug_info` - Type information
- `.debug_line` - Line number tables
- `.debug_str` - String table
- `.debug_abbrev` - Abbreviation tables
## Exercise 6: Stripping Symbols
Production binaries are often "stripped" to reduce size:
```bash
make stripped
ls -l withdebug stripped
# Try to profile the stripped binary
perf record ./stripped 500 5000
perf report
```
The stripped binary loses symbol information too!
## Exercise 7: Separate Debug Files
In production, you can ship stripped binaries but keep debug info separate:
```bash
# Extract debug info to separate file
objcopy --only-keep-debug withdebug withdebug.debug
# Strip the binary
strip withdebug -o withdebug.stripped
# Add a link to the debug file
objcopy --add-gnu-debuglink=withdebug.debug withdebug.stripped
# Now perf can find the debug info
perf record ./withdebug.stripped 500 5000
perf report
```
This is how Linux distros provide `-dbg` or `-debuginfo` packages.
## Discussion Questions
1. **Why don't we always compile with `-g`?**
- Binary size (can be 2-10x larger)
- Exposes source structure (security/IP concerns)
- Some optimizations may be affected (though `-O2 -g` works well)
2. **Does `-g` affect performance?**
- Generally no: debug info is stored in separate sections
- Not loaded unless a debugger attaches
- Some edge cases with frame pointers
3. **What about release vs debug builds?**
- Debug build: `-O0 -g` (no optimization, full debug)
- Release build: `-O2 -g` (optimized, with symbols)
- Stripped release: `-O2` then `strip`
## Key Takeaways
1. **Always compile with `-g` during development**
2. **Debug symbols don't meaningfully affect runtime performance**
3. **Without symbols, profilers show useless hex addresses**
4. **Production: ship stripped binaries, keep debug files for crash analysis**
## Bonus: Flamegraph Generation
```bash
# Record with call graph
perf record -g ./withdebug 500 5000
# Generate flamegraph (requires FlameGraph scripts)
perf script | /path/to/FlameGraph/stackcollapse-perf.pl | /path/to/FlameGraph/flamegraph.pl > profile.svg
```

View File

@@ -0,0 +1,88 @@
/*
* Scenario 5: Debug Symbols - The Missing Map
* ============================================
* This program has multiple functions calling each other.
* We'll compile it with and without debug symbols to show the difference.
*
* Compile WITHOUT debug symbols:
* gcc -O2 -o nodebug program.c
*
* Compile WITH debug symbols:
* gcc -O2 -g -o withdebug program.c
*
* EXERCISES:
* 1. perf record ./nodebug && perf report (see hex addresses)
* 2. perf record ./withdebug && perf report (see function names)
* 3. perf annotate -s compute_inner (see source code!)
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
/* Deliberately NOT inlined to show in profiler */
__attribute__((noinline))
double compute_inner(double x, int iterations) {
double result = x;
for (int i = 0; i < iterations; i++) {
result = sin(result) * cos(result) + sqrt(fabs(result));
result = result * 1.0001 + 0.0001;
}
return result;
}
__attribute__((noinline))
double compute_middle(double x, int iterations) {
double sum = 0.0;
for (int i = 0; i < 10; i++) {
sum += compute_inner(x + i, iterations);
}
return sum;
}
__attribute__((noinline))
double compute_outer(int n, int iterations) {
double total = 0.0;
for (int i = 0; i < n; i++) {
total += compute_middle((double)i / n, iterations);
}
return total;
}
__attribute__((noinline))
void process_data(int *data, int size) {
/* Some additional work to show in the profile */
long sum = 0;
for (int i = 0; i < size; i++) {
for (int j = 0; j < 100; j++) {
sum += data[i] * j;
}
}
printf("Data checksum: %ld\n", sum % 10000);
}
int main(int argc, char *argv[]) {
int n = 1000;
int iterations = 10000;
if (argc > 1) n = atoi(argv[1]);
if (argc > 2) iterations = atoi(argv[2]);
printf("Running with n=%d, iterations=%d\n", n, iterations);
/* Allocate some data */
int *data = malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
data[i] = i * 17 + 3;
}
/* Do some computation */
double result = compute_outer(n, iterations);
printf("Computation result: %f\n", result);
/* Process data */
process_data(data, n);
free(data);
return 0;
}