The conversion process from C language to BIN files: C source code (.c) → Preprocessing → Preprocessed C file (.i) → Compilation → Assembly file (.s) → Assembly → Object file (.o) → Linking → Executable file (ELF/PE) → Format conversion → Pure BIN file (.bin);
Core toolchain: Preprocessor (cpp) → Compiler (cc1) → Assembler (as) → Linker (ld) → Format converter (objcopy);
Key role: Each step “reduces the readability of the code while enhancing the executability for machines”, ultimately transforming human logic into a sequence of 0s and 1s that hardware can directly execute.
1. The Essential Relationship Between C Language and BIN Files
C Language: A high-level programming language that uses human-readable syntax (such as if, for, functions) to describe the logic and functionality of programs. It cannot be executed directly by the CPU and must be “translated” into machine-understandable instructions by a compiler.
BIN File: A binary file that stores machine code (a sequence of 0s and 1s) that the CPU can execute directly, as well as static data required for program execution (such as global variables and constants). It lacks human readability but can be directly loaded and executed by hardware (such as flashing in embedded systems or loading in PC operating systems).
Core relationship: C language is the “source code”, and the BIN file is the “final executable machine code file”. The transformation between the two is achieved through the process of compilation → assembly → linking → format conversion.
2. The Complete Conversion Process from C Language to BIN Files
The entire process is divided into four core steps, each completed by corresponding tools (using the GCC toolchain as an example, suitable for Linux/embedded development; similar tools for Windows are MSVC):
Step 1: Preprocessing — Expanding Source Code
Input: C language source code file (.c, such as main.c)
Tool: C preprocessor (cpp, can be invoked in GCC using gcc -E)
Core operations:
1. Expand include header files (insert the content of header files directly into the source code);
2. Replace define macros (substitute macro names with corresponding constants/expressions);
3. Handle conditional compilation (#if/#else/#endif, retaining only the code that meets the conditions);
4. Remove comments (// and /*…*/), adding line numbers and file name identifiers (for subsequent debugging).
Output: Preprocessed C file (.i, such as main.i), still a human-readable text file but without preprocessing directives.
Example: Original code (main.c):
include <stdio.h>define MAX 10int main() { printf("MAX: %d\n", MAX); return 0;}
Preprocessed (main.i, simplified version):
// Expand all content of stdio.h (omitted thousands of lines...) int main() { printf("MAX: %d\n", 10); // Macro MAX replaced with 10 return 0; }
Step 2: Compilation — Generate Assembly Code
Input: Preprocessed C file (.i)
Tool: C compiler (cc1, can be invoked in GCC using gcc -S)
Core operations:
1. Syntax analysis: Check if the code syntax is valid (such as matching parentheses, undefined variables);
2. Semantic analysis: Check if the code logic is reasonable (such as type mismatches, incorrect number of function parameters);
3. Intermediate code generation: Convert C code into platform-independent intermediate code (such as three-address code);
4. Target code generation: Convert intermediate code into assembly language code specific to the CPU architecture (such as x86, ARM assembly).
Output: Assembly language file (.s, such as main.s), human-readable (contains assembly instructions such as mov, add, call).
Example (ARM architecture assembly, main.s simplified version):
.file "main.c" ; Source file name .text ; Code section (stores instructions) .section .rodata ; Read-only data section (stores constants).LC0: .string "MAX: %d\n" ; String constant .text .align 2 .global main ; Global function main (needs to be exposed during linking) .type main, %function ; Declare main as functionmain: stmfd sp!, {fp, lr} ; Save stack frame (function call stack operation) add fp, sp, #4 ; Establish stack frame pointer mov r1, #10 ; Store 10 in register r1 (second parameter) ldr r0, .L3 ; Load string address into r0 (first parameter) bl printf ; Call printf function mov r0, #0 ; Return value 0 stored in r0 ldmfd sp!, {fp, lr} ; Restore stack frame bx lr ; Function return.L3: .word .LC0 ; Address of string .LC0 .size main, .-main ; Size of main function
Step 3: Assembly — Generate Object File (Machine Code)
Input: Assembly language file (.s)
Tool: Assembler (as, can be invoked in GCC using gcc -c)
Core operations: Translate assembly instructions into corresponding machine code for the CPU (each assembly instruction corresponds to one or more bytes of 0s and 1s), while:
1. Segmenting: Code is stored in the .text segment, read-only data in the .rodata segment, and global variables in the .data segment (initialized) or .bss segment (uninitialized, only occupies space without storing data);
2. Generating symbol table: Record function names (such as main) and variable names (such as global variables) corresponding to their “relative addresses” (absolute addresses not yet determined).
Output: Object file (.o, such as main.o), a binary file (not human-readable), but contains machine code and symbol table, with unresolved function call address dependencies (such as printf address not yet determined).
Step 4: Linking — Merge Object Files + Resolve Dependencies
Input: Multiple object files (.o, such as main.o), system libraries (such as C standard library libc.so/libc.a, which includes implementations of functions like printf)
Tool: Linker (ld, can be triggered directly in GCC using gcc)
Core operations:
1. Merging segments: Combine all object files’ .text, .data, .rodata, etc. segments into a single entity;
2. Symbol resolution: Based on the symbol table, find the actual address of each undefined symbol (such as printf) from the system library;
3. Relocation: Correct the “relative addresses” (such as the jump address for call printf) in the object file to “absolute addresses” (such as the actual memory address of printf in libc);
4. Generate executable file: Add file header (describing segment information, entry address, etc., for use by the operating system during loading).
Output: Executable file (such as a.out in Linux, ELF format; .exe in Windows, PE format), essentially a “binary file with a file header” that can be loaded and executed by the operating system.
Key note: If the program only depends on its own code (no external library calls), the linker only merges its own .o files;
If functions like printf, malloc, etc. from the standard library are called, the linker must link the C standard library (libc), otherwise it will report an “undefined symbol” error.
Step 5: Format Conversion (Optional) — Generate Pure BIN File
Background: Although the ELF and PE formats in Linux and Windows contain machine code, they also include additional information such as file headers and segment tables (for use by the operating system during loading); embedded systems (such as STM32, 51 microcontrollers) require a pure machine code image (without additional information) to be directly flashed into Flash for execution.
Input: Executable file in ELF/PE format (such as a.out)
Tool: Format conversion tool (in GCC, use objcopy; in embedded cross-compilation, use arm-linux-gnueabihf-objcopy)
Core operations: Extract the pure binary content of segments such as .text (code), .data (initialized data), .rodata (read-only data) from the executable file, concatenate them in memory address order, and generate a pure bin file without a file header.
Output: Pure binary file (.bin, such as app.bin), which can be directly loaded and executed by embedded hardware (such as flashing to microcontroller Flash via JTAG).
Example command (ARM cross-compilation environment):
# Convert ELF format executable file app.elf to pure bin file app.binarm-linux-gnueabihf-objcopy -O binary app.elf app.bin