DEV Community

ddupard
ddupard

Posted on

8051 What does SDCC do part 1 ?

1. Introduction and Problem Statement

A good way to learn what a compiler really does when transforming a C source code into a binary is to disassemble the binary and compare it with the C source code. It is especially true for 8 bits microcontrollers like the 8051.

In order to test SDCC we are going to use the following C source code.

/* ========================================================================== *
 * Universal Test Corpus - Heterogeneous Architecture Analysis          *
 * ========================================================================== */


#include <stdint.h>

// 1. Global variables (testing absolute/relative addressing modes)
volatile uint32_t global_var_32 = 0xDEADBEEF;
volatile uint8_t  global_var_8  = 0x42;
const    char     string_const[] = "TARGET_STRING";

// 2. Function with parameter passing and local variables (stack / Frame Pointer test)
int32_t callee_function(int16_t a, int16_t b) {
    volatile int32_t local_result = 0;

    // Basic and mixed arithmetic operations (8, 16, 32 bits)
    local_result += (int32_t)(a * b);
    local_result -= (int32_t)(a / (b | 1)); // Avoid division by zero

    // Shift tests and logical operations (highly variable depending on ISAs)
    local_result = (local_result << 2) ^ 0x55AA55AA;
    local_result = (local_result >> 1) | (int32_t)global_var_8;

    return local_result;
}

// 3. Main function grouping complex control flows
int main(void) {
    volatile int32_t accumulator = 0;
    int16_t i;

    // Loop test (Conditional jumps, decrement, comparison tests)
    for (i = 0; i < 10; i++) {
        if (i == 5) {
            accumulator += 100;
        } else {
            accumulator += i;
        }
    }

    // Multiple branching test (Switch / Jump Table or cascaded if-else)
    switch (global_var_8) {
        case 0x10:
            accumulator += 10;
            break;
        case 0x20:
            accumulator += 20;
            break;
        default:
            accumulator -= 5;
            break;
    }

    // Function call (Stack management, save registers Link Register/PC)
    accumulator += callee_function((int16_t)accumulator, 3);

    // Pointer and indirect memory access test
    volatile uint32_t *ptr = (volatile uint32_t *)&global_var_32;
    *ptr = (uint32_t)accumulator;

    // Terminal infinite loop (classic for raw binaries / microcontrollers)
    while (1) {
        accumulator ^= *ptr;
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

To compile it, we will use SDCC which produces an Intel HEX file. This file will be transformed in a ROM file using either objcopy or makebin (see below).

sdcc  "$SRC/test1.c" -o "8/test1_8051" 
objcopy -I ihex -O binary "8/test1_8051" "8/bin/test1_8051_bin" 
makebin -p "8/test1_8051" "8/bin/test1_8051.rom"
Enter fullscreen mode Exit fullscreen mode

Moreover, to simulate the 8051, I use the MCU 8051 IDE (command mcu8051ide).

2. 8051 Presentation

The standard 8051 comes with a 4kb rom and a 128 bytes ram. It seems so tiny when you compare it with 32 or 64 bits processors, so every byte is precious especially in RAM.

Here is a concise list of the 8051 registers with a brief explanation for each:

  • A (Accumulator): The primary 8-bit register for all arithmetic, logical, and data transfer operations.
  • B: An 8-bit secondary register used primarily in conjunction with the accumulator for multiplication and division operations.
  • DPTR (Data Pointer): A 16-bit register (split into DPH and DPL) used for external RAM and program memory (ROM) addressing.
  • PC (Program Counter): A 16-bit register that tracks the memory address of the next instruction to be executed.
  • SP (Stack Pointer): An 8-bit register pointing to the top of the stack, used for managing subroutine calls and saving return addresses.
  • PSW (Program Status Word): An 8-bit register containing status flags such as the Carry bit, Overflow bit, and register bank selection bits.
  • R0 – R7 (Working Registers): Eight general-purpose 8-bit registers grouped into selectable banks, used for temporary data storage and pointer operations.

Beyond its core registers and instruction set, the standard 8051 architecture provides essential hardware peripherals designed for embedded control.

It features four 8-bit bidirectional I/O ports (Port 0 to Port 3) which can be used for both data input/output and special functions like external memory addressing or communication.

For timing and event counting, the chip integrates two 16-bit timer/counters (Timer 0 and Timer 1) configurable in multiple modes.

Additionally, the 8051 implements a flexible interrupt structure supporting five distinct sources (external interrupts INT0 and INT1, timer overflows TF0 and TF1, and the serial port receive/transmit interrupt), each vectoring to a fixed program memory address to handle real-time events efficiently

3. 8051 The instruction set

The 8051 instruction set is rudimentary yet extremely direct.

A. Data Transfer Instructions (14 base mnemonics)

  • MOV – Move byte (register, direct, indirect, immediate)
  • MOVC – Move code memory (program ROM to accumulator)
  • MOVX – Move external data memory (RAM/IO to/from accumulator)
  • PUSH – Push byte onto stack
  • POP – Pop byte from stack
  • XCH – Exchange accumulator with byte
  • XCHD – Exchange lower-order nibble indirect

B. Arithmetic Instructions (8 base mnemonics)

  • ADD – Add to accumulator
  • ADDC – Add to accumulator with carry
  • SUBB – Subtract from accumulator with borrow
  • INC – Increment by 1 (accumulator, data pointer, registers, RAM)
  • DEC – Decrement by 1
  • MUL – Multiply A by AB
  • DIV – Divide A by B
  • DA – Decimal adjust accumulator

C. Logical Instructions (6 base mnemonics)

  • ANL – Logical AND
  • ORL – Logical OR
  • XRL – Logical Exclusive-OR
  • CLR – Clear accumulator or bit
  • CPL – Complement accumulator or bit
  • SWAP – Swap nibbles within the accumulator

D. Rotate & Shift Instructions (4 base mnemonics)

  • RL – Rotate accumulator left
  • RLC – Rotate accumulator left through carry
  • RR – Rotate accumulator right
  • RRC – Rotate accumulator right through carry

E. Boolean / Bit-Manipulation Instructions (10 base mnemonics)

  • SETB – Set bit to 1
  • MOV – Move bit data (listed under data transfer, used heavily in bit space)
  • JC – Jump if carry is set
  • JNC – Jump if carry is not set
  • JB – Jump if bit is set
  • JNB – Jump if bit is not set
  • JBC – Jump if bit is set and clear bit
  • ANL – Bitwise logical AND with carry
  • ORL – Bitwise logical OR with carry
  • CPL – Bit complement

6. Program Branching / Control Transfer Instructions (15 base mnemonics)

  • ACALL – Absolute subroutine call (2KB range)
  • LCALL – Long subroutine call (64KB range)
  • RET – Return from subroutine
  • RETI – Return from interrupt
  • AJMP – Absolute jump (2KB range)
  • LJMP – Long jump (64KB range)
  • SJMP – Short jump (relative offset)
  • JMP – Indirect jump relative to DPTR or PC (JMP @A+DPTR)
  • JZ – Jump if accumulator is zero
  • JNZ – Jump if accumulator is not zero
  • CJNE – Compare and jump if not equal
  • DJNZ – Decrement and jump if not zero
  • NOP – No operation

All these instructions use a 1-byte base opcode, though the full instruction may span 1, 2, or 3 bytes depending on the addressing mode and operands. For the 8051, the total number of instructions is 111 which use 255 opcodes. The only one not used is 0xA5.

To understand the difference between the 57 instructions in the instruction set and the total number of 111 instructions, let's take the example of the 24 arithmetic instructions.

The 24 arithmetic instructions of the 8051 are broken down into 8 families of mnemonics. Combining these mnemonics with their various addressing modes (accumulator, registers, direct or indirect memory, immediate values) yields a total of 24 distinct operation formats.Here is the complete breakdown categorized by function:

1. Addition (8 instructions)

  • ADD A, R: Adds the contents of register $R0\text{--}R7$ to the Accumulator ($A$).
  • ADD A, direct: Adds the contents of an internal direct memory address to $A$.
  • ADD A, @ri: Adds the value pointed to by $R0$ or $R1$ to $A$.
  • ADD A, #data: Adds an immediate value (constant) to $A$.
  • ADDC A, R: Adds register $R0\text{--}R7$ to $A$ along with the Carry flag ($C$).
  • ADDC A, direct: Adds a direct memory address to $A$ along with the Carry flag.ADDC A, @ri: Adds the memory contents pointed to by $R0$ or $R1$ to $A$ along with the Carry flag.
  • ADDC A, #data: Adds an immediate value to $A$ along with the Carry flag.

2. Subtraction (4 instructions)

  • SUBB A, R: Subtracts the value of register $R0\text{--}R7$ and the Carry flag ($C$) from $A$.
  • SUBB A, direct: Subtracts a direct memory address and the Carry flag from $A$.SUBB A, @ri: Subtracts the value pointed to by $R0$ or $R1$ and the Carry flag from $A$.
  • SUBB A, #data: Subtracts an immediate value and the Carry flag from $A$.

3. Increment (5 instructions)

  • INC A: Increments the Accumulator ($A = A + 1$).
  • INC R: Increments one of the registers $R0\text{--}R7$.
  • INC direct: Increments the contents of a direct memory address.
  • INC @ri: Increments the value pointed to by $R0$ or $R1$.
  • INC DPTR: Increments the 16-bit Data Pointer ($DPTR$).

4. Decrement (4 instructions)

  • DEC A: Decrements the Accumulator ($A = A - 1$).
  • DEC R: Decrements one of the registers $R0\text{--}R7$.
  • DEC direct: Decrements the contents of a direct memory address.
  • DEC @ri: Decrements the value pointed to by $R0$ or $R1$.

5. Multiplication and Division (2 instructions)

  • MUL AB: Multiplies registers $A$ and $B$ ($A \times B$). The 16-bit result is stored in the $B:A$ pair (High byte in $B$, Low byte in $A$).
  • DIV AB: Divides $A$ by $B$ ($A / B$). The quotient is stored in $A$ and the remainder in $B$.

6. BCD Adjustment (1 instruction)

*DA A: Decimal Adjust Accumulator. Adjusts the binary result in the Accumulator to yield a valid BCD (Binary Coded Decimal) value following an addition.

7. Comparison with the x86_64

As a comparison, the x86_64 architecture (CISC) has accumulated decades of backward compatibility since the original 8086. The exact number depends on how you count, because a single instruction can vary based on data size (8, 16, 32, or 64 bits), registers used, and prefixes applied:

  • Basic logical instructions or mnemonics: There are approximately 1,500 distinct instructions (according to Intel's XED decoding tool, which counts instruction classes from AAA to XTEST, including extensions).

  • Instruction forms / Effective opcodes: If you take into account all variations in operand size, addressing modes, and massive extensions (MMX, SSE, AVX, etc.), you arrive at more than 6,000 instruction variations.

Unlike the 8051, where the base opcode always fits into a single byte, x86_64 opcodes can span multiple bytes (often prefixed by 0x0F, 0x38, etc., or modified by REX bytes to switch to 64-bit mode), resulting in significantly more complex hardware decoding.

4. Memory-Mapped Registers and Direct Addressability

A unique architectural feature of the 8051 is that its Special Function Registers (SFRs) are directly mapped into the upper Special Function Register memory space (addresses 0x80 to 0xFF). In this architecture, core hardware registers—such as the Stack Pointer (SP at 0x81), the Accumulator (ACC at 0xE0), or I/O ports (like P0 at 0x80)—are not isolated processor entities. Instead, they can be accessed directly by their physical hex addresses or by their standard mnemonic names. This memory-mapping allows standard data movement and bit-manipulation instructions (e.g., MOV 0x81, #0x07 or SETB 0x88) to directly configure peripherals and CPU status flags, streamlining low-level hardware control without requiring specialized bus-management instructions.

Now that the general presentation of the 8051 architecture is over, it's time to dive into what SDCC generates

5. SDCC: The stub

In bare-metal environments and microcontroller architectures like the 8051, a stub acts as the foundational initialization bridge between hardware reset and the high-level application code. Because microcontrollers do not feature a host operating system to set up memory spaces or execution contexts, the startup stub is responsible for critical low-level chores: it sets the Stack Pointer (SP) to define the valid stack boundary, clears internal RAM to ensure predictable initial variable states, and configures hardware segments before safely jumping to the user's main routine.

Right below you will find the stub generated by SDCC.

CSEG AT 0000h
  0000 020006       LJMP L0001

L0004:
  0003 020147       LJMP L0005

L0001:
  0006 758118       MOV SP, #18h
  0009 1202C6       LCALL L0002
  000C E582         MOV A, DPL
  000E 6003         JZ L0003
  0010 020003       LJMP L0004

L0003:
  0013 7900         MOV R1, #0h
  0015 E9           MOV A, R1
  0016 4400         ORL A, #0h
  0018 601B         JZ L0025
  001A 7A00         MOV R2, #0h
  001C 9002D8       MOV DPTR, #02D8h
  001F 7801         MOV R0, #1h
  0021 75A000       MOV P2, #0h
L0027:
  0024 E4           CLR A
  0025 93           MOVC A, @A+DPTR
  0026 F2           MOVX @R0, A
  0027 A3           INC DPTR
  0028 08           INC R0
  0029 B80002       CJNE R0, #0h, L0026
  002C 05A0         INC P2
L0026:
  002E D9F4         DJNZ R1, L0027
  0030 DAF2         DJNZ R2, L0027
  0032 75A0FF       MOV P2, #0FFh
L0025:
  0035 E4           CLR A
  0036 78FF         MOV R0, #0FFh
L0028:
  0038 F6           MOV @R0, A
  0039 D8FD         DJNZ R0, L0028
  003B 7800         MOV R0, #0h
  003D E8           MOV A, R0
  003E 4400         ORL A, #0h
  0040 600A         JZ L0029
  0042 7901         MOV R1, #1h
  0044 75A000       MOV P2, #0h
  0047 E4           CLR A
L0030:
  0048 F3           MOVX @R1, A
  0049 09           INC R1
  004A D8FC         DJNZ R0, L0030
L0029:
  004C 7800         MOV R0, #0h
  004E E8           MOV A, R0
  004F 4400         ORL A, #0h
  0051 600C         JZ L0031
  0053 7900         MOV R1, #0h
  0055 900001       MOV DPTR, #0001h
  0058 E4           CLR A
L0032:
  0059 F0           MOVX @DPTR, A
  005A A3           INC DPTR
  005B D8FC         DJNZ R0, L0032
  005D D9FA         DJNZ R1, L0032
L0031:
  005F 7508EF       MOV 8h, #0EFh
  ...
Enter fullscreen mode Exit fullscreen mode

The stub goes from the very first instruction (at address 0000) till the instruction located at address 005D.
The first real instruction related to the program starts at 005F. The stub takes a little bit less than 95 bytes before doing something related to the program. The 95 bytes contain code which is nearly useless for our program. In fact the only 2 things interesting in the 95 bytes are:

  • the initialization of the SP register
  • the reset of memory

so it could be easily replaced with the following instructions ( 9 bytes )

    758118      MOV SP, #18h              
    E4          CLR A
    78FF        MOV R0, #0FFh
L0028:
    F6          MOV @R0, A
    D8FD        DJNZ R0, L0028
Enter fullscreen mode Exit fullscreen mode

meaning that we could reuse 86 bytes of the stub without changing the code of the program.

Stub Optimization as a Stealth Vector for verify_system_integrity (see EEPROM Hijacking on 8051 Architecture)

As we observed when analyzing SDCC's compiled output, the default 8051 startup stub wastes nearly 95 bytes on generic memory clearing routines that are largely redundant for custom firmware deployments. Instead of appending our security checks—such as the verify_system_integrity routine—to the end of the program where space may be constrained or easily monitored, we can surgically repurpose these wasted bytes within the startup stub itself. By replacing the bloated initialization code with our compact 9-byte sequence (MOV SP and the RAM-clearing loop), we free up over 86 bytes of pristine ROM space right at the very beginning of execution. Injecting the verify_system_integrity hook directly into this reclaimed space ensures that the integrity check runs before any main application logic or EEPROM operations take place, achieving both zero-overhead optimization and a stealthier boot-time security gatekeeper.

6. the 8051 Today

The 8051 is still used today, although its role has evolved considerably since its launch. Even though newer 32-bit and 64-bit architectures dominate modern computing, the 8051 architecture remains active and omnipresent across several fields:

Everyday Embedded Electronics and Appliances

This is one of the best-known strongholds: you find the 8051 in almost everything around you—from your television remote control to washing machines, microwave ovens, fridges, smart cards, car key fobs, and power supplies, chosen for its reliability, low power, and cost efficiency.

Industrial Embedded Systems and Legacy Hardware

In industry, the golden rule is "if it works, don't change it." Many control devices, automation systems, smart sensors, and power management units incorporate standard 8051 cores or modern enhanced derivatives. Replacing these proven embedded systems often requires costly redesign and recertification.

Modern Descendants and Silicon Vendors

The base architecture also survives through modernized and supercharged versions. Major semiconductor manufacturers (such as NXP, Silicon Labs, and Maxim/Analog Devices) offer high-performance derivatives featuring single-cycle execution, higher frequencies, expanded flash memory, and advanced peripherals like USB, CAN, and ADCs while maintaining software compatibility.

Embedded Education and Hobbyists

The academic and hobbyist community keeps the 8051 extremely alive. As one of the most widely taught microcontrollers in engineering schools, it continues to serve as the foundational platform for learning bare-metal programming, assembly language, and hardware interfacing fundamentals.

7. Conclusion

This article was an introduction of what the SDCC compiler does. Even in this small part we saw, that the compiler adds some code which could be easily removed because unuseful. We will see the same phenomenon in the next article.

Top comments (0)