CRT (C Runtime) “glue code” refers to a set of pre-compiled object files (typically crt1.o, crti.o, crtn.o, crtbegin.o, and crtend.o) that are automatically linked with your program. They “glue” the operating system’s process loader to your main() function by handling low-level setup (stack, environment) and high-level initialization (global constructors).
Execution Order Summary:
_start(Entry Point)__libc_start_main(Standard C Library setup)__libc_csu_init/_init(Generic initialization hooks).init_array(Global constructors/C++ initializers)main()(Your code)
Detailed Explanation
1. What is CRT Glue Code?
The “glue” consists of startup files provided by the OS (glibc) and the compiler (GCC/Clang). They wrap your code to ensure the environment is ready before main runs.
crt1.o(Start): Contains the actual entry point symbol_start. It sets up the very first stack frame and arguments before calling into the C library.crti.o(Init Prologue): Contains the “header” (prologue) instructions for the_initand_finifunctions.crtn.o(Init Epilogue): Contains the “footer” (epilogue/return) instructions for_initand_fini.crtbegin.o/crtend.o: Provided by the compiler (e.g., GCC) to handle language-specific features like C++ global constructors and destructors.
2. Execution Order of an Executable
Here is the step-by-step flow from the moment the kernel hands over control to the moment your code runs:
Step 1: The Kernel & _start
- The OS loads the ELF binary and jumps to the address defined as the Entry Point (usually
_startinsidecrt1.o). _startclears the frame pointer (ebp/rbp) to mark the end of the stack trace.- It grabs
argc,argv, andenvp(environment variables) from the stack. - It calls the helper function
__libc_start_main.
Step 2: __libc_start_main (The Coordinator)
- Located in
glibc. - It performs critical setup: initializes threading (pthreads), security cookies (stack canary), and registers the cleanup function (
rtld_fini) for the dynamic linker. - Crucially, it receives pointers to the program’s initialization functions (often
__libc_csu_initor similar) and calls them.
Step 3: Initialization Phase (The “Before Main” Logic)
This is where the statement in your prompt comes into play. The CRT initialization code calls these in specific order:
_initfunction:- Constructed from
crti.o+ system-specific init code +crtn.o. - Historically used for setup, though now deprecated in favor of
.init_array. - Executes strictly BEFORE
.init_array.
- Constructed from
.init_arrayprocessing:- The CRT iterates through the
.init_arraysection of the ELF file. - This array contains function pointers to C++ global constructors and functions marked with
__attribute__((constructor)). - These execute one by one.
- The CRT iterates through the
Step 4: main()
- Once all
.init_arrayfunctions return,__libc_start_mainfinally calls yourmain(argc, argv, envp).
Step 5: Cleanup
- When
mainreturns,exit()is called. - This triggers functions in
.fini_array(destructors) and finally_fini.
CRT Symbols
- _start
- deregister_tm_clones
- register_tm_clones
- __do_global_dtors_aux
- frame_dummy
- __libc_csu_init
- __libc_csu_fini
AFL++ Instrument
Unfortunately, in a stripped binary, you cannot reliably exclude these specific functions by name because their names are stripped. You’ll need to rely on heuristics and location to identify and exclude them.
Here’s how to approach it, along with why it’s more challenging:
The Challenge with Stripped Binaries
- No Symbol Names: deregister_tm_clones, register_tm_clones, __do_global_dtors_aux, frame_dummy are completely unknown.
- Variable Location: Their exact position and size can vary significantly between compiler versions, optimization levels, and even different target architectures (though you’re on ARM64).
- Interdependence: These functions might be called by other CRT code, or call into other parts of the CRT that you also need to exclude.
Heuristics and Exclusion Strategies
- Entry Point Analysis (Still Key): As discussed before, the initial analysis of _start is crucial. It gives you the addresses of __libc_csu_init and potentially __libc_csu_fini. These are the “entry points” into the CRT initialization and cleanup sequences.
- __libc_csu_init and __libc_csu_fini:
- __libc_csu_init: This function is where deregister_tm_clones and register_tm_clones are often called. If you exclude __libc_csu_init (by finding its address from _start), you are likely excluding the blocks that contain calls to these tm_clones functions.
- __libc_csu_fini: This function is the counterpart for cleanup.
- __do_global_dtors_aux:
- Purpose: This is responsible for calling the destructors of global C++ objects.
- Heuristic: It’s often called by __libc_csu_fini or the program’s exit handler. It typically involves iterating through a list of destructors and calling them. You might be able to identify it by looking for loops that call functions via indirect jumps or register manipulation. This is harder to pinpoint reliably.
- frame_dummy:
- Purpose: This is a compiler-generated function used for stack frame setup, often in functions that require frame pointers (even if -fomit-frame-pointer is used).
- Heuristic: It’s a very small function (often just a few instructions). It’s usually called by other CRT or compiler-generated functions. It’s quite hard to identify purely by disassembled code unless it’s a very obvious pattern and you can trace its caller.
Practical Approach for Stripped Binaries:
- Prioritize the “Big Wins”:
- Entry Point (_start): Always exclude.
- __libc_csu_init and __libc_csu_fini: Exclude by finding their addresses from _start. This is your highest priority, as it covers a lot of CRT code.
- The cbz Safety Net:
- This is your primary defense. Even if you miss some edge-case CRT blocks, the cbz instruction in your instrumentation callback will prevent the program from crashing. It’s extremely cheap performance-wise.
- Heuristic Exclusion for __do_global_dtors_aux and frame_dummy (Advanced):
- __do_global_dtors_aux: This is the most likely one you might want to try to exclude after the csu functions. It’s usually called near the end of the program’s life. You might identify it by looking for code that loads a function pointer from a list and calls it. This requires some rudimentary static analysis or symbolic execution.
- frame_dummy: This is the hardest to identify generically. It’s so small and often inlined or called indirectly. Trying to exclude it without symbols is generally not worth the effort compared to the robustness of the cbz instruction.
Recommendation for Your Stripped Binary:
- Implement the cbz in your bb_callback. This is non-negotiable for reliability in stripped binaries.
- Implement the _start and __libc_csu_init/fini exclusion using Capstone analysis. This will eliminate the most problematic CRT code.
- Do NOT try to reliably exclude __do_global_dtors_aux or frame_dummy by heuristics alone in a stripped binary. The complexity and unreliability outweigh the benefits. The cbz will handle these cases gracefully.
By following this, you get the best of both worlds:
- Safety: The cbz prevents crashes from uninitialized pointers.
- Performance: You avoid instrumenting the main CRT initialization/cleanup entry points.
- Simplicity: You don’t have to build fragile heuristics for every single minor CRT helper function.
PS: For a single object (executable or shared library), the .init section is processed and executed before the functions in the .init_array section.
The typical execution order for initialization routines in an ELF binary is as follows:
- Functions in the
.preinit_arraysection (only in executables). - The code in the
.initsection. - Functions pointed to by entries in the
.init_arraysection.
This entire sequence happens as part of the C/C++ runtime startup (often within a function like __libc_init_array()), before the program’s main() function is called. The .init section is a legacy mechanism, and modern libraries and compilers (like GCC using __attribute__((constructor))) primarily use the more flexible .init_array section.