Showing posts with label cpp_debug. Show all posts
Showing posts with label cpp_debug. Show all posts

Nov 2, 2025

[debugging techniques] C++ on Nightmare Mode


SIGBUS Memory alignment bug


Unsafe:
  • The Lie: The static_cast<const std::uint32_t *>(p) is a promise to the compiler. You are saying, "Trust me, this void* pointer p definitely points to a memory location that is properly aligned for a std::uint32_t."
  • The Requirement: On many architectures (like ARM, SPARC, and MIPS), a std::uint32_t (a 4-byte integer) must be located at a memory address that is a multiple of 4. For example, 0x1000, 0x1004, or 0x1008 are all aligned. An address like 0x1001 is unaligned.
  • The Machine Code: When the compiler sees the dereference (*), it trusts your promise and generates the most efficient instruction to load a 4-byte integer. On ARM, this might be a single LDR (Load Register) instruction.
  • The Crash: If p happens to be an unaligned address (like 0x1001), the CPU's LDR instruction cannot execute. The processor hardware itself traps this invalid operation and triggers an exception. The operating system handles this exception by sending a SIGBUS (Bus Error) signal to your program, which causes it to crash.
(Note: This code often "works" on x86/x86-64 (like most PCs), but it's not actually safe. x86 hardware is more permissive and will handle the unaligned access, but it incurs a significant performance penalty.)
std::uint32_t decode(const void* p) {
  return boost::endian::little_to_native(*static_cast<const std::uint32_t *>(p));
} 
Safe:
  • The Safe Variable: You first declare a local variable, std::uint32_t v. Because this variable is declared on the stack, the compiler guarantees that v itself is properly aligned. Its address (&v) will be a multiple of 4.
  • The Honest Copy: std::memcpy makes no assumptions about alignment. Its job is to copy bytes, one way or another.
  • The Machine Code: The compiler knows that p (the source) might be unaligned and &v (the destination) is aligned. It will generate a sequence of safe instructions to accomplish this. This often means it will load the 4 bytes from p one byte at a time (e.g., using four LDRB - Load Byte instructions) and then reassemble them into the aligned v variable.
  • The Result: No unaligned 4-byte integer load is ever attempted. The CPU is only asked to do byte-level access, which is always safe and has no alignment.
This memcpy pattern is the standard, portable, and optimizer-friendly way to safely read a value from a potentially unaligned buffer.
std::uint32_t decode(const void* p) {
  std::uint32_t v = 0; // local variable, guaranteed aligned.
  std::memcpy(&v, p, sizeof(v));
  boost::endian::little_to_native_inplace(v);
  return v;
}

This was so much a known issue thus in C++20 we have std::bit_cast
#include <bit>     // Required for std::bit_cast
#include <array>   // Required for std::array
#include <cstdint> // Required for std::uint32_t
// ... boost::endian headers ...

std::uint32_t decode(const void* p) {
  // 1. Cast 'p' to a pointer to an array of 4 bytes.
  //    This cast itself is just a reinterpretation; no memory is read.
  // Reason cast to const unsigned char* won't work due to
  // std::bit_cast has safety check making sure that the sizeof(From) == sizef(To)
  // Thus sizeof(const unsigned char*) is just one byte, not 4 bytes.
  const auto* byte_ptr = reinterpret_cast<const std::array<unsigned char, 4>*>(p);

  // 2. Dereference the pointer.
  //    This performs a *safe* copy of 4 bytes from the (potentially unaligned)
  //    address 'p' into a local, *aligned* 'bytes' object.
  std::array<unsigned char, 4> bytes = *byte_ptr;

  // 3. Reinterpret the bits of the byte array as a uint32_t.
  std::uint32_t v = std::bit_cast<std::uint32_t>(bytes);

  // 4. Fix endianness, same as before.
  boost::endian::little_to_native_inplace(v);
  return v;
}


Classic time-travel compiler optimization bug

This is due to compiler seeing we actually dereference p at foo(*p) thus if not crash, p must not be null_ptr, thus eliminate if (!p) check completely. 
Raymond Chen has a post[Undefined behavior can result in time travel (among other things, but time travel is the funkiest)] on this.

There's compiler flag no-delete-null-pointer-checks which is also enabled by default by Chromium project.


Compiler might not honor your code

float get_first_element(__m128 v)
{
  return _mm_cvtss_f32(v);
}

_Z17get_first_elementDv4_f:
.LFB6474:
.cfi_startproc
endbr64
ret
.cfi_endproc



Standard ambiguity

When you are faced with surprising cross-platform behavior, it can save you a lot of time to refer back to the standard (every word matters)
// get the epoch
auto epoch =
  std::chrono::system_clock::now().time_since_epoch();
// send it to a peer
send_network(epoch);
Until C++ 20
  • The epoch of system_clock is unspecified, but most implementations use Unix Time
    (i.e., time since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, not counting leap seconds).

Since C++ 20
  • system_clock measures Unix Time (i.e., time since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, not counting leap seconds).


Alignment

  • alignas(64) at the class level specifies that the object must be aligned on a 64 bytes boundary.
  • In C++ 20 you can use alignas() on fields, no need for manual padding.
Wrong:
// align on the struct, not the data members, thus false sharing still happening.
struct alignas(64) my_struct {
  std::atomic<int> one;
  std::atomic<int> two;
}; 

Correct:
p.s. alignas(std::hardware_destructive_interference_size) won't work on Mac compiler(as of 2025).
struct my_struct {
  alignas(std::hardware_destructive_interference_size) std::atomic<int> one;
  alignas(std::hardware_destructive_interference_size) std::atomic<int> two;
}; 

Lessons learned


i.e. malloc_trim shall be called manually before the thread that alloc the arena memory goes to sleep, otherwise, the allotted memory will not be return back to the system. Usually a thread_local context object could have solved this issue.









May 7, 2022

[CPPCON 2021][notes] Debugging/Developing techniques

Reference:
https://www.youtube.com/watch?v=M7fV-eQwxrY

Define bugs

  • System is subject to a set of requirements.
  • A software defect is a non-conformity to requirements.
  • Pre-Curr-Post condition violated.
  • A non-conformity is a failure to meet one or more requirements.
  • A defect is incorrect program data causes a non-conformity.
  • A symptom is observable evidence of a defect.
  • A deterministic defect is a defect that does not change its symptoms under a well-defined set of conditions.
  • In contrast, a non-deterministic defect is a defect that changes its symptoms from run-to-run under a well-defined set of conditions.


Terminology

  • A context is the totality of the environment is which a program that exhibits symptoms is running
  • A problem report describes one or more symptoms in some context
  • Analogous context is a replica of the original context
  • Lab is the setting that we have total control over the context
  • Field is the setting that we have minimal or no control over the context


Relationship

Problem report -> Symptoms <-> Defects


Challenges

  • Problem report can be unhelpful (feed back from the user)
  • Problem report may not indicate actual problem
  • Collecting program state data may be difficult (log/setting/dump)
  • Symptoms may not indicate the cause
  • Defects and symptoms change as repair progresses
  • Fixing one defect may introduce new defects (messy design/quick fix)
  • Symptoms can be difficult to reproduce


Debugging process

Tend to think debugging is a linear process; i.e.
  • Characterize and reproduce
  • Locate
  • Classify
  • Understand
  • Repair


In reality tips

  • Review problem report
  • Characterize and reproduce problem
  • Clone if possible
  • Reproduce problem (loop)
    • understand problem
    • locate problem
    • classify problem
    • gain insight
    • attempt to repair
  • Problem fixed; deliver



In detail

Characterizing

  • Determining the context in which symptoms were observed
  • Version number, platform, resources allocated, external interfaces, configuration data, etc.
  • Information that allows you to instantiate an analogous context


Reproducing

  • Instantiating an analogous context, in the lab, or in the field
  • Running enough of the program/system to observe the reported symptoms
  • Developing new/updating existing test assets to demonstrate the failure
  • Make sure looking at the correct source code.
Characterizing and reproducing a problem is vital to the debugging process.


Understanding

Gaining ENOUGH knowledge about a problem and the surrounding code, that you believe you can make changes to carry out a repair.

At a minimum

  • located the incorrect lines of code
  • why the code is incorrect, root cause?
  • check the proposed classification
  • formulated a set of proposed changes
  • determine how the proposed changes could affect the runtime state

Inspect and verify the associated test assets

  • The test cases or harnesses may be broken
  • Test data should demonstrate correct and incorrect behavior

The defect may not be where you expect it

  • Keep an open mind and be ready to question all parts of the program

Ask yourself where the defect is not

  • trying to prove the absence of a defect reveals the defect

Explain to people why there is a defect, and why your proposed fix will resolve the defect

  • A local guru or bobblehead could be helpful - reach out for help if necessary


Locate the problem

Employ good development practices at the outset

  • Practice iterative, incremental, bottom-up development
  • Add functionality in small sections of code
  • Create test assets for each new increment of functionality
  • Verify that new code doesn't cause previous test cases to fail
  • Verify that new code passes its own test cases
  • Practice defensive programming

Alas

  • Well-written and extensive test assets
  • Preferably the whole product does this, at a minimum your fixes should
  • Adds runtime overhead, which can hinder the search for non-deterministic problems

Use trace logging

  • Generating output describing the program state during execution
  • In simpler cases, instrument code with print statements
  • In more complex systems, take advantage of existing logging facilities

Alas

  • Great way to stay 'on the path' when developing new code
  • An easy first step in narrowing down a problem's scope

Use debugging and analysis tools

  • Compiler warnings
  • Static code analysis tools (cppcheck, etc.)
  • Interactive debugger(gdb, lldb, udb, etc.)
  • Time-travel debugger(gdb, rr, udb, etc.)
  • Sanitizers (asan, tsan, ubsan, etc.)
  • Dynamic program analyzers(valgrind, etc.)
  • tracers (strace, wireshark, etc.)

Alas

  • for deterministic problems
  • not always useful for non-deterministic problem

Enable and/or add assertions

  • verify pre/curr/post condition of a function call.
  • verify expected program state

Alas


Use backtracking

  • Try to understand the program state at each backward step

Alas

  • Good for very simple programs/small search with deterministic problems

Divide and conquire (binary search)

  • Pick section of code to examine
  • Place an assertion or set a breakpoint.
  • Repeat until reveals the defect

Problem simplification

  • Gradually and strategically remove/comment out sections of irrelevant code

Alas

  • useful for debugging crashes of release builds
  • work backwards from the end of the section

Make the problem worse

  • Magnifies the problem signal.

Alas

  • helpful in first step finding and understanding the problem

Scientific method

  • Form a hypothesis consistent with observations
  • Implement tests to refute the hypothesis
  • If refuted, form a new hypothesis with new tests

Alas

  • time consuming; especially for code base that unfamiliar with
  • effective for all problems


Problem Types

Deterministic problems

  • Review the logs
  • Add assertions
  • Use interactive debugger

Non-deterministic problems

  • Review the logs
  • Create a debug build and see if it also exhibits the same symptoms
  • Add assertions where needed to verify invariants
  • Add assertions; comment out code, divide-and-conquer
  • Make the problem WORSE to magnify the problem
  • try low-overhead debugging tools "$gcc -g -o2"



Steps

Classifying

  • Determining a defect's category
  • Useful in formulating a repair strategy
  • Important information in subsequent reviews when considering preventive actions

Syntax errors
Syntax warnings
Implementation errors
Logic errors
Configuration errors


Repairing the problem

  • Implementing the appropriate fixed.
  • Passing the tests
  • Tests should be well written
  • Minimize changes to the system - keep changes small and localized
  • Verify repairs against test assets
    • All new/update tests should pass
    • All other tests should pass

Delivery

  • Practice good version control
  • Don't include fixes for more than one problem in one commit
  • Don't include extraneous changes (e.g. new features) in fix commits
  • Include new/update test assets in the fix commits
  • Write commit comments clear and concise

Verify tests again

  • Double check all new/update tests pass
  • Double check all other tests pass

Create documentation for posterity

  • How the defect was noticed
  • The conditions under which the defect occurred - the context
  • Steps necessary to reproduce the defect - the analogous context
  • Techniques and tools used to localize the defect
  • Defect's category
  • Underlying root cause of the defect
  • Latent defects precluded by fixing this defect
  • Possible latent defects left unaddressed
  • Mistake made and recommendations for preventive actions



Developing new feature

  • Practice defensive programming
    • Assume the worst case could happen at any time.
  • Employ an appropriate iterative and incremental development process
    • Decide what needs to be achieved
    • Formulate a plan for the achievement
    • Understand the invariants, requirements, and context, then design the solution.
    • Implement the solution in small, discrete, testable chunks
    • Write code to verify invariants, pre-cur-post conditions and self-test complex components
  • Consider employing the principles of test-driven design
  • Employ good configuration management practice EVERYWHERE.

Oct 19, 2018

[C++][linux debugging] GDB wrap up - 2018

Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it? -- Brian Kernighan

GDB

manual: https://sourceware.org/gdb/current/onlinedocs/gdb/
Build atop ptrace.
When program being traced receives a signal, it is suspended and the tracer gets notified(through waitpid).

When the inferior receives a signal, it stops and gdb gets control.

Usually gdb returns to the prompt, but what it will do depends on the signal and how it is configured.

Two not so special signals:
  1. SIGINT is generated when hitting Ctrl-C
  2. 2. SIGTRAP is generated when the inferior hits a breakpoint or is single stepped.
https://sourceware.org/gdb/current/onlinedocs/gdb/TUI-Keys.html

-g and -O are orthogonal. 
-Og is optimised but doesn't mess up debug.
-ggdb3 is better than -g
$ clang++ -ggdb3 test.cpp
$ gdb a.out

TUI command:

$ list // list source code.
$ Ctrl-x-a // TUI Interface.
$ Ctrl-x-o // Move to source view.
$ Ctrl-x-s
$ Ctrl-x-2 // cycle through different UI layout.
$ Ctrl-x-1 // cycle back to single window layout.
$ Ctrl-l // refresh screen.
$ Ctrl-p/n //previous/next command.
$ shell ps

traverse:

$ next
$ tbreak // tmp breakpoint.
$ rbreak // regex breakpoint.
$ command // list commands when breakpoint hits.
$ silent // suppress output.
$ save breakpoints // save bp to script.
$ save history // as it is.
$ info line foo.c:42 // show PC for line.
$ info line * $pc // show line begin/end for current Program Counter.
$ frame // Select and print a stack frame.

signals:

$ info signals
set signal disposition:
$ handle SIGINT stop print pass

Breakpoints and watch points:

$ watch foo // stop when foo is modified
$ watch -l foo // watch location
$ rwatch foo // stop when foo is read
$ watch foo thread 42 // stop when thread 42 modifies foo
$ watch foo if foo > 42 // stop when foo is > 42

Thread apply:

$ thread apply 1-4 print $sp // $sp as stack pointer.
$ thread apply all backtrace
$ thread apply all backtrace full
$ info threads // show the current running program threads info

Dynamic Printf:

Using dprintf to put printf in the code without recompiling,
e.g
$ dprintf mutex_lock, "%p, %u\n", m, m-val
Control how the printf happen:
$ set dprintf-style gdb|call|agent
$ set dprintf-function fprintf
$ set dprintf-channel mylog

Calling inferior function:

$ call foo()
$ print foo()
$ print bar + foo // C++ operator overload detected by gdb.
$ print errno

Below calls malloc() as well.
$ call strcpy(buffer, "Hi!\n")


Catchpoints:

Like breakpoints but catch certain events, e.g C++ exceptions
$ catch catch // stop when C++ exceptions caught.
$ catch syscall nanosleep // stop at nanosleep system call.
$ catch syscall 100 // stop at system call number 100.


Remote debugging:

$ gdbserver localhost:2000 ./a.out // server
$ target remote localhost:2000 // client


Multiprocess Debugging:

$ set follow-fork-mode child|parent
$ set detach-on-fork off
$ info inferiors
$ inferior N
$ set follow-exec-mode new|same
$ add-inferior <cnt> <name>
$ remove-inferior N
$ clone-inferior
$ print $_inferior


GDB with embedded python interface:

https://stackoverflow.com/questions/12574253/c-gdb-python-pretty-printing-tutorial
$ python gdb.execute()
$ python gdb.parse_and_eval()
$ python help('gdb')


Valgrind:

$ valgrind ./a.out
With build-in GDB:
$ valgrind --vgdb=full --vgdb-error=0 ./a.out

Tools:
Cachegrind: cache profiler, simulates L1, D1, L2 caches
Callgrind: like Cachegrind but with call-graphs
Massif: heap profiler
Helgrind: Spot race conditions in multi-thread program.
DRD: Data Race Detector. Like Helgrind, use less memory.
Lackey : Simple Valgrind tool that does various kinds of basic program measurement.



Sanitizers:

Build into the compiler.
AddressSanitizer, MemorySanitizer, ThreadSanitizer, LeakSanitizer
$ clang++ -g -fsanitize=address test.cpp



rr

has limitation, only on baremetal Intel x86 CPU.


ftrace:

Function Tracer, trace various kernel functions.
Lots of pre-defined events
Controlled through /sys/kernel/debug/tracing



strace:

based on ptrace
$ strace -k cmd   // show backtrace for each syscall.




ltrace:

trace dynamic library calls of a process.


perf trace:

build on perf. faster than strace.



fortify:

memcpy, strcpy, etc. do bounds checking where it can.
Slow.
$ clang++ -D_FORTIFY_SOURCE=1