Lesson 22-Custom Low-Level Toolchain Development Techniques

Custom Assembler Development: Lexer Design

Regular Expression Rules Example:

# x86 Assembly Lexical Rules Example
import re

TOKEN_SPEC = [
    ('LABEL', r'[a-zA-Z_][a-zA-Z0-9_]*:'),     # Label definition
    ('INSTR', r'mov|add|sub|jmp|call'),         # Instruction set
    ('REG',  r'r[0-9]+|eax|ebx|ecx|edx'),       # Registers
    ('NUM',  r'0x[0-9a-fA-F]+|\d+'),            # Numbers
    ('MEM',  r'\[.*?\]'),                       # Memory references
    ('SEP',  r'[,\s]+')                         # Separators
]

lexer = re.compile('|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPEC))

Lexical Analysis Process:

  • Input preprocessing (removing comments/whitespace)
  • Pattern matching and token generation
  • Error recovery mechanism
  • Symbol table construction

Parser Implementation

Recursive Descent Parsing Example:

// Simple assembly syntax parsing framework
void parse_instruction() {
    Token *tok = lexer_next();
    if (tok->type == TOKEN_INSTR) {
        parse_operand_list();   // Parse operands
        emit_machine_code();    // Generate machine code
    } else {
        syntax_error("Expected instruction");
    }
}

void parse_operand_list() {
    parse_operand();  // First operand
    while (current_token() == ',') {
        lexer_consume(',');  // Skip separator
        parse_operand();     // Subsequent operands
    }
}

Abstract Syntax Tree (AST) Structure Example:

struct ASTNode {
    enum { INSTR_NODE, OPERAND_NODE } type;
    union {
        struct {
            char *opcode;
            struct ASTNode *operands[3];
        } instr;
        struct {
            enum { REG_OPERAND, IMM_OPERAND, MEM_OPERAND } kind;
            union {
                char *reg;
                int imm;
                struct {
                    char *base;
                    int offset;
                } mem;
            } u;
        } operand;
    } u;
};

Custom Linker Development

ELF File Format Parsing

ELF Header Structure:

typedef struct {
    unsigned char e_ident[16];   // ELF magic number
    uint16_t e_type;             // File type
    uint16_t e_machine;          // Target architecture
    uint32_t e_version;          // Version number
    uint32_t e_entry;            // Entry point address
    uint32_t e_phoff;            // Program header offset
    uint32_t e_shoff;            // Section header offset
    uint32_t e_flags;            // Processor-specific flags
    uint16_t e_ehsize;           // ELF header size
    uint16_t e_phentsize;        // Program header entry size
    uint16_t e_phnum;            // Number of program headers
    uint16_t e_shentsize;        // Section header entry size
    uint16_t e_shnum;            // Number of section headers
    uint16_t e_shstrndx;         // Section name string table index
} Elf32_Ehdr;

Section Relocation Handling:

void relocate_section(Elf32_Shdr *rel_sec, Elf32_Shdr *sym_sec, uint8_t *base) {
    for (int i = 0; i < rel_sec->sh_size / rel_sec->sh_entsize; i++) {
        Elf32_Rel *rel = (Elf32_Rel *)(base + rel_sec->sh_offset + i * rel_sec->sh_entsize);
        uint32_t *target = (uint32_t *)(base + rel->r_offset);
        Elf32_Sym *sym = &((Elf32_Sym *)base)[sym_sec->sh_offset / sizeof(Elf32_Sym) + ELF32_R_SYM(rel->r_info)];

        switch (ELF32_R_TYPE(rel->r_info)) {
            case R_386_PC32:
                *target = sym->st_value + rel->r_addend - (uint32_t)target;
                break;
            // Other relocation types...
        }
    }
}

PE File Format Parsing

PE Header Key Fields:

typedef struct {
    uint32_t Signature;                 // "PE\0\0"
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32;

typedef struct {
    uint16_t Machine;
    uint16_t NumberOfSections;
    uint32_t TimeDateStamp;
    uint32_t PointerToSymbolTable;
    uint32_t NumberOfSymbols;
    uint16_t SizeOfOptionalHeader;
    uint16_t Characteristics;
} IMAGE_FILE_HEADER;

Import Table Parsing:

void parse_import_table(IMAGE_NT_HEADERS32 *nt_hdr, uint8_t *base) {
    IMAGE_DATA_DIRECTORY *import_dir = &nt_hdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    IMAGE_IMPORT_DESCRIPTOR *import_desc = (IMAGE_IMPORT_DESCRIPTOR *)(base + import_dir->VirtualAddress);

    while (import_desc->Name) {
        char *dll_name = (char *)(base + import_desc->Name);
        printf("Import DLL: %s", dll_name);

        uint32_t *iat = (uint32_t *)(base + import_desc->FirstThunk);
        while (*iat) {
            printf(" IAT Entry: 0x%p", (void *)*iat);
            iat++;
        }
        import_desc++;
    }
}

Custom Debugger Development

ptrace System Call: Process Control Flow

// Debugger main loop
while (1) {
    waitpid(child_pid, &status, 0);
    if (WIFEXITED(status)) break;

    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, child_pid, NULL, ®s);

    // Set breakpoint
    if (should_break(regs.rip)) {
        uint64_t orig_insn = read_memory(child_pid, regs.rip);
        write_memory(child_pid, regs.rip, 0xCC);  // INT3 instruction
        ptrace(PTRACE_SETREGS, child_pid, NULL, ®s);
        ptrace(PTRACE_CONT, child_pid, NULL, NULL);
        continue;
    }

    ptrace(PTRACE_SINGLESTEP, child_pid, NULL, NULL);
}

// Resume execution
void continue_execution(pid_t pid, struct user_regs_struct regs) {
    uint64_t orig_insn = get_original_insn(regs.rip);
    write_memory(pid, regs.rip, orig_insn);
    ptrace(PTRACE_SETREGS, pid, NULL, ®s);
    ptrace(PTRACE_CONT, pid, NULL, NULL);
}

Breakpoint Implementation Techniques

Software Breakpoint Principle:

  • Save original instruction bytes
  • Write INT3 (0xCC) instruction
  • On exception:
    • Suspend target process
    • Notify debugger
    • Restore original instruction
    • Single-step execute
    • Reinsert breakpoint

Hardware Breakpoint Utilization:

// x86 Debug Register Setup
struct {
    uint64_t dr0;  // Breakpoint address 0
    uint64_t dr1;  // Breakpoint address 1
    uint64_t dr2;  // Breakpoint address 2
    uint64_t dr3;  // Breakpoint address 3
    uint64_t dr6;  // Status register
    uint64_t dr7;  // Control register
} debug_regs;

// Set execution breakpoint
void set_exec_breakpoint(uint64_t addr) {
    debug_regs.dr0 = addr;
    debug_regs.dr7 |= (1 << 0);  // Enable DR0 execution breakpoint
    ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[0]), &debug_regs);
}

Custom Emulator Development

CPU Instruction Set Emulation

Virtual CPU Core Structure:

struct CPUState {
    uint64_t regs[16];               // General-purpose registers
    uint64_t rip;                    // Instruction pointer
    uint64_t rsp;                    // Stack pointer
    uint64_t cr0;                    // Control register
    uint64_t cr3;                    // Page directory base address
    uint8_t *memory;                 // Physical memory space
    struct {
        uint64_t base;
        uint64_t limit;
        uint16_t selector;
    } segment[6];                    // Segment registers
};

Instruction Execution Loop:

void cpu_exec(CPUState *cpu) {
    while (1) {
        uint8_t opcode = fetch_byte(cpu);
        switch (opcode) {
            case 0x90:  // NOP
                cpu->rip += 1;
                break;
            case 0xB8:  // MOV EAX, imm32
                cpu->regs[0] = fetch_dword(cpu);
                cpu->rip += 5;
                break;
            // Other instruction handlers...
            default:
                printf("Unknown opcode: 0x%02X", opcode);
                exit(1);
        }
    }
}

MMU Virtualization Implementation

Page Table Traversal Algorithm:

uint64_t translate_address(CPUState *cpu, uint64_t virt_addr) {
    // CR3 points to top-level page table
    uint64_t pml4e = cpu->cr3 + ((virt_addr >> 39) & 0x1FF) * 8;
    uint64_t pml4_entry = read_physical_memory(cpu, pml4e);
    if (!(pml4_entry & 0x1))  // P bit not set
        raise_page_fault(virt_addr);

    uint64_t pdpte = (pml4_entry & ~0xFFF) + ((virt_addr >> 30) & 0x1FF) * 8;
    uint64_t pdpt_entry = read_physical_memory(cpu, pdpte);
    // Continue traversing PD and PT...

    // Final physical address calculation:
    return (pt_entry & ~0xFFF) | (virt_addr & 0xFFF);
}

Page Fault Exception Handling:

void handle_page_fault(CPUState *cpu, uint64_t fault_addr) {
    if (is_mapped(cpu, fault_addr)) {
        // Mapped but insufficient permissions
        raise_access_violation();
    } else {
        // Allocate new physical page
        uint64_t phys_page = alloc_physical_page();
        map_page(cpu, fault_addr, phys_page, PAGE_PRESENT | PAGE_READWRITE);
    }
}

Custom Instruction Set Design

Microcode Architecture Design

Microinstruction Format Example:

| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|----|----|----|----|----|----|----|----|
| OP | R1 | R2 | R3 | M1 | M2 | F1 | F0 |
  • OP (3 bits): Opcode
  • R1–R3 (3 bits): Register selection
  • M1–M2 (2 bits): Memory access mode
  • F1–F0 (2 bits): Function extension

Microcode Execution Engine:

void execute_microcode(uint8_t microcode) {
    uint8_t op = (microcode >> 5) & 0x7;
    uint8_t r1 = (microcode >> 2) & 0x7;
    uint8_t r2 = (microcode >> 1) & 0x3;

    switch (op) {
        case 0x0:  // ADD
            cpu->regs[r1] += cpu->regs[r2];
            break;
        case 0x1:  // MOV
            cpu->regs[r1] = cpu->regs[r2];
            break;
        // Other operations...
    }

    if (microcode & 0x1) {  // Check F0 bit
        handle_flags(cpu);
    }
}

ISA Extension Techniques

Custom Instruction Encoding Example:

; Assume a new vector addition instruction is added:
VADDPS xmm0, xmm1, xmm2   ; xmm0 = xmm1 + xmm2
; Corresponding machine code might be:
; 0xC4 0xE2 0x79 0x58 0xC2 ; EVEX prefix + opcode

Dynamic Instruction Decoding:

void decode_instruction(uint8_t *code) {
    uint8_t prefix = code[0];
    if (prefix == 0xC4) {  // EVEX prefix
        uint8_t opcode = code[4];
        switch (opcode) {
            case 0x58:  // VADDPS
                execute_vaddps(code+5);
                break;
            // Other EVEX instructions...
        }
    } else {
        // Traditional instruction decoding...
    }
}

Toolchain Integration Techniques

Debug Information Generation

DWARF Debug Information Example:

.debug_info <1> <0x0000000b>: Abbrev Number: 1 (DW_TAG_compile_unit)
<2> DW_AT_producer      : "Custom Assembler "
<3> DW_AT_language      : DW_LANG_Assembly
<4> DW_AT_name          : "example.asm "
<5> DW_AT_low_pc        : 0x00000000
<6> DW_AT_high_pc       : 0x0000001a
<1> <0x0000001f>: Abbrev Number: 2 (DW_TAG_label)
<2> DW_AT_name          : "loop_start "
<3> DW_AT_decl_file     : 1
<4> DW_AT_decl_line     : 5
<5> DW_AT_low_pc        : 0x00000005

Performance Profiling Integration

Performance Counter Interface:

// Read CPU cycle counter
uint64_t read_cycles() {
    uint32_t lo, hi;
    asm volatile (
        "rdtsc" : "=a"(lo), "=d"(hi)
    );
    return ((uint64_t)hi << 32) | lo;
}

// Measure code segment execution time
void profile_code() {
    uint64_t start = read_cycles();
    // Code under test...
    uint64_t end = read_cycles();
    printf("Cycles: %lu", end - start);
}

By mastering these low-level techniques, developers can:

  • Build customized development toolchains
  • Implement simulators for special architectures
  • Develop security research tools
  • Optimize critical system components
  • Conduct computer architecture research
  • Contribute to core development of open-source projects

Legal Disclaimer:
The techniques described in this document are intended for lawful purposes only. Unauthorized reverse engineering and tool development may violate laws such as the Regulations on Computer Software Protection and other relevant regulations. All experiments must be conducted in authorized environments.

Share your love