Tearing Down eCos RTOS Embedded System [KR]
2026-04-08 06:29:04

TL;DR


eCos 기반 임베디드 시스템을 플래시 덤프부터 부팅 시퀀스까지의 분석 과정을 설명합니다.

Flash Dump


PCB
eCos 기반 라우터 타겟에는 SoC (System-on-Chip), SPI SOP-8 NOR 플래시 칩, UART (Universal Asynchronous Receiver-Transmitter) 인터페이스 및 무선 LAN 칩이 탑재되어 있었습니다. 플래시 메모리 덤프를 위해 NOR 플래시 칩을 칩오프하기로 했습니다.

FlashROM
FlashROM-off

NOR 플래시는 BoHong BH25D16A 칩이었으며 핀 구성도는 아래와 같습니다:

FlashROM-pinmap

  • 1: /CS (칩 선택)
  • 2: DO (직렬 데이터 출력)
  • 3: /WP (LOW로 설정 시 쓰기 방지)
  • 4: VSS (접지)
  • 5: DI (직렬 데이터 입력)
  • 6: SCK (직렬 클록)
  • 7: NC (연결 안 됨)
  • 8: VCC (공급 전압)

BH25D16A는 SPI 버스의 전용 (공유되지 않는) 접근이 필요한 환경을 위해 설계되었으므로 다른 장치와 버스를 공유하기 위한 /HOLD 핀이 없습니다. 또한 플래시 메모리 읽기만 수행하기 때문에 /WP 핀은 연결하지 않았습니다 (읽기 전용 작업에는 필요하지 않음).

SPI


┌──────────┐                     ┌──────────┐
│          │──── MOSI (SDI) ────>│          │
│          │<─── MISO (SDO) ─────│          │
│  Master  │──── SCLK (CLK) ────>│  Slave   │
│          │──── CS/SS (LOW) ───>│          │
└──────────┘                     └──────────┘

SPI (Serial Peripheral Interface)는 하나의 마스터가 하나 이상의 슬레이브 장치와 전이중 방식으로 통신할 시 사용하는 동기식 직렬 통신 프로토콜입니다. 메모리 덤프의 경우, 마스터는 SPI 프로그래머이고 슬레이브는 메모리 칩이 됩니다.

Pin Direction Description
MOSI(Master Out Slave In) 마스터 → 슬레이브 마스터의 데이터 송신 선
MISO(Master In Slave Out) 슬레이브 → 마스터 슬레이브의 데이터 송신 선
SCLK(Serial Clock) 마스터 → 슬레이브 마스터의 클럭 신호 선
CS/SS(Chip Select / Slave Select) 마스터 → 슬레이브 LOW로 설정 시 슬레이브 선택

Clock Signal


SCLK  __/‾\_/‾\_/‾\_/‾\__
MOSI  -[1]-[0]-[1]-[1]---
        ↑   ↑   ↑   ↑
      클럭 엣지마다 1비트씩 전송함

클록 신호는 장치들에게 언제 데이터를 읽거나 써야 하는지를 알려주는 타이밍 신호 역할을 수행하여 여러 장치가 통신하는 동안 동기화 상태를 유지하도록 합니다. 마스터는 클럭을 정의하고, 슬레이브는 클럭 엣지 (SPI 모드 0/3의 상승 엣지) 에서 데이터를 샘플링합니다.

SPI Communication Diagram


CS   ‾‾‾‾\___________________________________/‾‾‾‾
        ↑ CS LOW (통신 시작)                   ↑ CS HIGH (통신 종료)

SCLK _______/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_______
            1   2   3   4   5   6   7   8
            ↑ 클럭 엣지마다 1비트씩 전송함

MOSI ────[MSB]──────────────────────────[LSB]─────  (마스터 → 슬레이브)
MISO ────[MSB]──────────────────────────[LSB]─────  (슬레이브 → 마스터)
         ↑ 전이중

BH25D16A의 최대 클럭 주파수는 일반 읽기 시 55MHz, 고속/듀얼 읽기 시 108MHz입니다. 저는 CH341A 프로그래머를 사용하여 NOR 플래시와 SPI 통신했습니다. flashrom의 spispeed는 2MHz로 고정이며, 이는 NOR 플래시의 최대 클럭 주파수 범위 내에 있으므로 별도의 속도 조정 없이 플래시 메모리를 읽을 수 있었습니다.

FlashROM-dump
flashrom
읽어온 펌웨어의 안정성을 확인하기 위해 총 두 번 바이너리를 추출한 후 각 바이너리의 MD5 해시값을 비교했습니다.

md5sum
binwalk firmware

바이너리의 MD5 해시는 모두 동일했습니다. 펌웨어에는 gzip으로 압축된 데이터와 LZMA로 압축된 리소스가 포함되어 있습니다. LZMA로 압축된 리소스는 부팅 시퀀스에서 RAM으로 압축 해제됩니다. 펌웨어 정적 분석을 수행하기 전에 올바른 메모리 주소에 매핑된 세그먼트를 생성하기 위해서 SoC의 메모리 맵을 식별해야 합니다.

그래서 EMI (Electromagnetic Interference) 차폐막을 제거하고 SoC 모델을 확인했습니다. 해당 SoC는 MIPS 아키텍처 기반의 RTL8197F 제품군에 속합니다.

EMI-Shield open

SoC

Boot Sequence Overview


boot-sequence-diagram

Boot Stage 1


ROM memory map
RTL8197F 데이터시트에 따르면, Stage 1 Boot ROM 코드는 XIP(eXecute In Place) 기술을 사용하여 가상 주소 0xB0000000에서 실행됩니다.

Stage 1 BL vector table
Stage 1 BL reset_entry

Stage 1 Boot ROM 코드는 부팅 로그를 출력하고, D-Cache (데이터 캐시) 와 I-Cache (명령어 캐시) 를 무효화하고, Stage 1.5 부트로더 (0xB0007000) 를 DRAM (0x80100000) 에 로드한 후 해당 위치로 점프합니다.
그래서 다음과 같이 새로운 DRAM 세그먼트를 생성했습니다:

Stage 1.5 BL segment

Boot Stage 1.5


DRAM:80100000     # int dram_entry()
DRAM:80100000    dram_entry:
DRAM:80100000                    li      $t0, unk_8010B4C0
DRAM:80100008                    nop
DRAM:8010000C                    addi    $t0, 0x2000
DRAM:80100010                    nop
DRAM:80100014                    move    $sp, $t0
DRAM:80100018                    nop
DRAM:8010001C                    jal     boot_stage1_5_main
DRAM:80100020                    nop
DRAM:80100024                    nop
DRAM:80100028                    nop
DRAM:8010002C                    nop
DRAM:8010002C     # End of function dram_entry
...

int boot_stage1_5_main()
{
  int v0; // $a0
  int v1; // $a1
  int v2; // $a2
  int v3; // $a3
  unsigned int v5; // [sp+20h] [-Ch] BYREF
  int v6; // [sp+24h] [-8h] BYREF

  v5 = 0x7FFFFFFF;
  v6 = 0x7FFFFFFF;
  gzip_decompress(byte_80101D60, &v5, 0x80000000, &v6); // gzip_decompress(0x80101D60, &in_size, 0x80000000, &out_size)
  post_decompress_init();                       // D-cache writeback + I-cache invalidate — synchronize released code cache
  return MEMORY[0x80000000](v0, v1, v2, v3);    // jalr 0x80000000 -> Stage 2 Bootloader("RTL8197F-VG boot version:614")
}

Stage 1.5 부트로더는 gzip으로 압축된 Stage 2 부트로더 (0x80101D60) 를 KSEG0 (0x80000000) 으로 압축 해제하고, 명령어 캐시와 데이터 캐시를 동기화 (D-Cache 쓰기 + I-Cache 무효화) 한 후 실행합니다.

Boot Stage 2


DRAM:80000000     # Segment type: Pure code
DRAM:80000000                    .text # DRAM     // vector table
DRAM:80000000                    b       loc_8000040C     # DATA XREF: sub_80003130+70↓o
DRAM:80000004                    nop
DRAM:80000004     # ---------------------------------------------------------------------------
DRAM:80000008                    .byte    0
...

DRAM:8000040C     # ---------------------------------------------------------------------------
DRAM:8000040C
DRAM:8000040C    loc_8000040C:                            # CODE XREF: DRAM:80000000↑j
DRAM:8000040C                    mtc0    $zero, WatchLo   # Memory reference trap address low bits
DRAM:80000410                    mtc0    $zero, WatchHi   # Memory reference trap address high bits
DRAM:80000414                    mtc0    $zero, Cause     # Cause of last exception
DRAM:80000418                    mfc0    $t0, SR          # Status register
DRAM:8000041C                    li      $at, 0x1000001F
DRAM:80000424                    or      $t0, $at
DRAM:80000428                    xori    $t0, 0x1F
DRAM:8000042C                    mtc0    $t0, SR          # Status register
DRAM:80000430                    ehb
DRAM:80000434                    mtc0    $zero, Count     # Timer Count
DRAM:80000438                    mtc0    $zero, Compare   # Timer Compare
DRAM:8000043C                    lui     $sp, 0x8070
DRAM:80000440                    nop
DRAM:80000444                    li      $s0, 0x80018B90
DRAM:8000044C                    li      $s1, 0x80437F10
DRAM:80000454                    move    $t0, $s0
DRAM:80000458
DRAM:80000458    loc_80000458:                            # CODE XREF: DRAM:80000460↓j
DRAM:80000458                    sw      $zero, 0($t0)
DRAM:8000045C                    addi    $t0, 4
DRAM:80000460                    bne     $t0, $s1, loc_80000458
DRAM:80000464                    nop
DRAM:80000468                    nop
DRAM:8000046C                    jal     sub_8000B7E0    // main initialization
DRAM:80000470                    nop
DRAM:80000474                    nop
DRAM:80000478
DRAM:80000478    loc_80000478:                            # CODE XREF: DRAM:loc_80000478↓j
DRAM:80000478                    b       loc_80000478
DRAM:8000047C                    nop
...
int sub_8000B7E0()
{
  sub_8000B7A8();
  return ((int (*)(void))((unsigned int)sub_8000C328 & 0xDFFFFFFF))();
}

int sub_8000C328()
{
  unsigned __int32 v0; // $at
  _BYTE v2[16]; // [sp+20h] [-10h] BYREF

  MEMORY[0x800247F0] = 0;
  MEMORY[0x800247F4] = 0;
  init_rt_uart_1();
  sub_8000E294();
  sub_80000CA4();
  sub_8000E270();
  sub_8000C1B8();
  set_gpio_register(19, 1);                     // maybe set uart baudrate to 38400
  if ( check_PABCD_DAT_register() )
  {
    sub_8000D5E0();
    recovery_mode();
  }
  print_boot_info(0);
  v0 = _mfc0(0xCu, 0);
  _mtc0(0xCu, 0, v0 | 1);                       // enable interrupt
                                                // mtc0 SR |= 1
  _ehb();
  sub_80000DF4();
  sub_8000D5E0();
  sub_8000CFFC(4);
  sub_8000D5E0(
    "port link 0x%x 0x%x 0x%x 0x%x\n",
    MEMORY[0xBB80412C],
    MEMORY[0xBB804130],
    MEMORY[0xBB804134],
    MEMORY[0xBB804138]);
  sub_8000D5E0("irq:0x%x\n", MEMORY[0xB8003000]);
  sub_80002E3C();
  sub_80002E3C();
  MEMORY[0xB8003000] &= ~0x8000u;
  MEMORY[0x80018BC4] = 0;
  return firmware_load(1, 0, (int)v2);
}

int __fastcall firmware_load(int a1, int a2, int a3)
{
  _BYTE v5[8]; // [sp+18h] [-8h] BYREF

  if ( !a1 )
    sub_8000E22C();
  if ( sub_8000E4C0(1200000000) == 1 )
  {
    sub_8000D3D4("\nEscape booting by user\n");
    sub_8000E22C();
  }
  if ( !check_firmware_integrity(a3, v5) )
    sub_8000E22C();
  return load_firmware_n_jump(MEMORY[0x80018BC4], a3);
}

Stage 2 Bootloader: Firmware Integrity Verification


int __fastcall check_firmware_integrity(int a1, int a2)
{
  int result; // $v0
  _BYTE v3[8]; // [sp+18h] [-14h] BYREF
  unsigned int v4; // [sp+20h] [-Ch]
  unsigned int v5; // [sp+24h] [-8h]

  MEMORY[0x80018BC4] = 0xB0020000;
  result = check_firmware_header(0xB0020000, a1, a2);
  if ( result == 2 )
  {
    sub_80008F40(v3, 0x20000, 16);
    return verify_rootfs_checksum(
             (HIBYTE(v5) | (v5 << 24) | ((v5 & 0xFF00) << 8) | ((v5 & 0xFF0000) >> 8))
           + (HIBYTE(v4) | (v4 << 24) | ((v4 & 0xFF00) << 8) | ((v4 & 0xFF0000) >> 8))
           - 1342177264);
  }
  return result;
}

int __fastcall check_firmware_header(int a1, _DWORD *a2)
{
  ...
  if ( !sub_80008F40(a2, a1, 16) )
  {
    sub_8000D3D4("flashread fail,addr:%x,size:%d\n", a1, 16);
    return 0;
  }
  a2[1] = HIBYTE(a2[1]) | (a2[1] << 24) | ((a2[1] & 0xFF00) << 8) | ((a2[1] & 0xFF0000u) >> 8);
  a2[2] = HIBYTE(a2[2]) | (a2[2] << 24) | ((a2[2] & 0xFF00) << 8) | ((a2[2] & 0xFF0000u) >> 8);
  a2[3] = HIBYTE(a2[3]) | (a2[3] << 24) | ((a2[3] & 0xFF00) << 8) | ((a2[3] & 0xFF0000u) >> 8);
  strncpy(&v13, "cs6c", 4);
  strncpy(&v14, "cr6c", 4);
  v5 = 1;
  if ( !strcmp(a2, &v13, 4) || (v5 = 2, !strcmp(a2, &v14, 4)) )
  {
      ...
      if ( a2[3] == v6 )
      {
        result = v5;
        if ( !v7 )
          return result;
        sub_8000D3D4("check failed, ret:%d, sum:%x\n", v5, v7);
        return 0;
      }
    }
    return 0;
  }
  sub_8000D3D4("No sys signature at %X!\n", a1 + 1342177280);
  return 0;
}

check_firmware_integrity 함수는 RealTek 펌웨어의 매직 바이트인 “cs6c”와 “cr6c”를 기준으로 펌웨어의 매직 바이트 무결성을 검사합니다.

  • cs6c: LZMA로 압축된 펌웨어 이미지
  • cr6c: 루트 파일 시스템 이미지

이 경우 펌웨어에는 “cs6c” 매직 바이트가 포함되어 있는데, 이는 루트 파일 시스템이 아닌 LZMA로 압축된 이미지임을 나타냅니다.

xxd firmware magic bytes

또한, 매직 바이트가 “cr6c”인지 여부에 따라 루트 파일 시스템 체크섬 값을 검증합니다.

int __fastcall load_firmware_n_jump(int a1, _DWORD *a2)
{
  unsigned int v5; // $a0
  unsigned int v6; // $a2
  int (*v7)(void); // $s0
  unsigned __int32 v8; // $at

  if ( !sub_80008F40(a2, a1, 16) )
    return sub_8000D3D4("sflashread fail,addr=%x,size=%d\n", a1, 16);
  v5 = HIBYTE(a2[1]) | (a2[1] << 24) | ((a2[1] & 0xFF00) << 8) | ((a2[1] & 0xFF0000u) >> 8);
  a2[1] = v5;
  a2[2] = HIBYTE(a2[2]) | (a2[2] << 24) | ((a2[2] & 0xFF00) << 8) | ((a2[2] & 0xFF0000u) >> 8);
  v6 = HIBYTE(a2[3]) | (a2[3] << 24) | ((a2[3] & 0xFF00) << 8) | ((a2[3] & 0xFF0000u) >> 8);
  a2[3] = v6;
  sub_80008F40(v5 | 0x20000000, a1 + 1342177296, v6 - 2);
  MEMORY[0xB8003000] = 0;
  MEMORY[0xB8003528] &= ~0x400000u;
  MEMORY[0xB8003528] &= ~0x4000000u;
  sub_8000D3D4("Jump to image start:0x%x...\n", a2[1]);
  v7 = (int (*)(void))a2[1];
  MEMORY[0xB8003000] = 0;
  MEMORY[0xB8003004] = 0;
  MEMORY[0xB8003114] = 0;
  MEMORY[0xB8000010] &= ~0x800u;
  MEMORY[0xBBDC0300] = -1;
  MEMORY[0xBBDC0304] = -1;
  v8 = _mfc0(0xCu, 0);
  _mtc0(0xCu, 0, (v8 | 1) ^ 1);
  _ehb();
  sub_8000B990();
  return v7();
}
RTL8197F-VG boot release version: ... (600MHz)
Mac addr:...
port link ...
Jump to image start:0x80700000...

무결성 검증이 완료되면, Kernel Decompressor를 KSEG0 (0x80700000) 에 로드하고 실행합니다.

Kernel Decompressor


// Analyzed by Claude Code Opus 4.6

DRAM:80700000     # Segment type: Pure code
DRAM:80700000                    .text # DRAM
DRAM:80700000                    nop                                                                                                                                                                        
DRAM:80700004                    nop
DRAM:80700008                                                                                                                                                                                               
DRAM:80700008     # =============== S U B R O U T I N E =======================================                                                                                                             
DRAM:80700008
DRAM:80700008     # Kernel decompressor entry. Loaded at DRAM 0x80700000 by Stage 2 bootloader.                                                                                                             
DRAM:80700008     # BSS clear (0x80860800-0x80860818), stack setup ($sp=0x80861818),                                                                                                                        
DRAM:80700008     # then j decompress_kernal_n_jump (skips prologue, enters at 0x807001E8).                                                                                                                 
DRAM:80700008                                                                                                                                                                                               
DRAM:80700008     # void __noreturn kernel_decompressor_entry()                                                                                                                                             
DRAM:80700008    kernel_decompressor_entry:                                                                                                                                                                 
DRAM:80700008                    li      $s0, 0x80860800          # BSS start                                                                                                                               
DRAM:80700010                    li      $s1, 0x80860818          # BSS end (24 bytes)                                                                                                                      
DRAM:80700018                    beq     $s0, $s1, loc_80700034                                                                                                                                             
DRAM:8070001C                    nop                                                                                                                                                                        
DRAM:80700020                    move    $t0, $s0                                                                                                                                                           
DRAM:80700024                                                                                                                                                                                               
DRAM:80700024    loc_80700024:                                    # BSS clear loop                                                                                                                          
DRAM:80700024                    sw      $zero, 0($t0)                                                                                                                                                      
DRAM:80700028                    addi    $t0, 4                                                                                                                                                             
DRAM:8070002C                    bne     $t0, $s1, loc_80700024                                                                                                                                             
DRAM:80700030                    nop                                                                                                                                                                        
DRAM:80700034                                                   
DRAM:80700034    loc_80700034:                                                                                                                                                                              
DRAM:80700034                    move    $t0, $s1                 # $t0 = 0x80860818                                                                                                                        
DRAM:80700038                    addi    $t0, 0x1000              # $t0 = 0x80861818
DRAM:8070003C                    move    $sp, $t0                 # $sp = 0x80861818                                                                                                                        
DRAM:80700040                    j       decompress_kernal_n_jump # enters at 0x807001E8 (skips prologue)
DRAM:80700044                    move    $a0, $t0                 # delay: $a0 = $sp base                                                                                                                   
DRAM:80700044     # End of function kernel_decompressor_entrys
...                                                                                                                                                                                                         
                                                                
DRAM:807001E8     # Main decompression routine. Entered at 0x807001E8 (mid-function, prologue skipped).                                                                                                     
DRAM:807001E8     # Prints "decompressing kernel:", calls LZMA wrapper to decompress
DRAM:807001E8     # eCos kernel to 0xA0000000 (KSEG1 uncached DRAM).                                                                                                                                        
DRAM:807001E8     # After LZMA wrapper returns to 0x80700220, boots kernel at 0xA0000600.                                                                                                                   
DRAM:807001E8     # Note: 0xA0000600 = 0x80000600 (same physical DRAM, uncached vs cached).                                                                                                                 
DRAM:807001E8                                                                                                                                                                                               
DRAM:807001E8    decompress_kernal_n_jump:                                                                                                                                                                  
DRAM:807001E8                    lui     $t8, 0x8086                                                                                                                                                        
DRAM:807001EC                    move    $s1, $a0                 # $s1 = 0x80861818 (stack base from entry)                                                                                                
DRAM:807001F0                    lui     $a0, 0x8070                                                                                                                                                        
DRAM:807001F4                    sw      $zero, 0x80860800        # clear decompressor status                                                                                                               
DRAM:807001F8                    lui     $s0, 0xB800                                                                                                                                                        
DRAM:807001FC                    lui     $t8, 0x8086                                                                                                                                                        
DRAM:80700200                    li      $a0, fmt                 # printf("decompressing kernel:\n")                                                                                                       
DRAM:80700204                    jal     sub_80700D34                                                                                                                                                       
DRAM:80700208                    sw      $s0, 0x80860804          # delay: store MMIO base 0xB8000000
DRAM:8070020C                    move    $a3, $zero               # $a3 = 0 (flags)                                                                                                                         
DRAM:80700210                    lui     $a2, 0x8100              # $a2 = 0x81000000 (output limit, 16MB)                                                                                                   
DRAM:80700214                    addiu   $a1, $s1, 0x1000         # $a1 = LZMA compressed data source                                                                                                       
DRAM:80700218                    jal     LZMA_decompress_engine_wrapper  # $ra = 0x80700220                                                                                                                 
DRAM:8070021C                    lui     $a0, 0xA000              # delay: $a0 = 0xA0000000 (dst, KSEG1 uncached)                                                                                                                                                                                          
...                                                                                                                                                                                                         
                                                                                                                                                                                                            
DRAM:80701458     # LZMA decompression wrapper. Frameless (no prologue, uses caller stack).                                                                                                                 
DRAM:80701458     # Parses LZMA header at 0x80702800 (props: 5D, dictSize=8MB),
DRAM:80701458     # calls LZMA_decompress engine, prints status messages.                                                                                                                                   
DRAM:80701458     # On success: prints "done, booting the kernel." and returns 0.                                                                                                                           
DRAM:80701458     # Return: jr $ra → 0x80700220 (back to decompress_kernal_n_jump).
DRAM:80701458                                                                                                                                                                                               
DRAM:80701458    LZMA_decompress_engine_wrapper:                
DRAM:80701458                    sw      $s3, 0xC0($sp)           # save callee regs to caller stack                                                                                                        
DRAM:8070145C                    sw      $s2, 0xBC($sp)                                                                                                                                                     
DRAM:80701460                    sw      $s1, 0xB8($sp)                                                                                                                                                     
DRAM:80701464                    sw      $s0, 0xB4($sp)                                                                                                                                                     
DRAM:80701470                    sw      $a0, 0x80860C20          # save dst = 0xA0000000                                                                                                                   
DRAM:8070148C                    sw      $a1, 0x80860C1C          # save lzma_src                                                                                                                           
DRAM:80701494                    sw      $a2, 0x80860C18          # save limit = 0x81000000                                                                                                                 
DRAM:80701484                    sw      $a3, 0x80860814          # save flags = 0                                                                                                                          
DRAM:80701490                    jal     sub_80700D34             # printf("Uncompressing...")                                                                                                              
DRAM:807014A0                    addiu   $s2, $t8, 0x2800         # $s2 = 0x80702800 (LZMA metadata)                                                                                                        
DRAM:807014AC                    lwl     $s0, 3($s2)              # load uncompressed size (big-endian)                                                                                                     
DRAM:807014BC                    wsbh    $s0                      # byte-swap: big-endian → little-endian                                                                                                   
DRAM:807014C4                    rotr    $s0, 0x10                # complete 32-bit endian swap                                                                                                             
DRAM:807014C8                    jal     memcpy_custom            # memcpy(sp+0x94, 0x80702808, 5)                                                                                                          
                                                                  # copy LZMA props: 5D 00 00 80 00                                                                                                        
...                                                                                                                                                                                                         
DRAM:807015D4                    jal     LZMA_decompress          # LZMA decode: ~1.4MB → 5.22MB                                                                                                            
                                                                  # output to 0xA0000000 (uncached DRAM)                                                                                                   
DRAM:807015DC                    beqz    $v0, success             # if result == 0 → success                                                                                                                
DRAM:807015E8                    li      $a1, "LZMA: Decoding error = %d\n"                                                                                                                                 
DRAM:807015EC                    jal     sprintf                  # format error message                                                                                                                    
...                                                                                                                                                                                               
                                                                                                                                                                                                            
success:                                                                                                                                                                                                    
DRAM:807015FC                    li      $a0, "done, booting the kernel.\n"
DRAM:80701604                    jal     sub_80700D34             # printf → UART output                                                                                                                    
DRAM:8070160C                    move    $v0, $zero               # return 0 (success)                                                                                                                      
DRAM:80701610                    lw      $ra, 0xCC($sp)           # restore $ra (= 0x80700220, set by jal at 0x80700218)                                                                                    
DRAM:8070162C                    jr      $ra                      # return to decompress_kernal_n_jump @ 0x80700220
DRAM:80701630                    addiu   $sp, 0xD0                # delay: restore caller's stack
// Generated by Claude Code Opus 4.6

kernel_decompressor_entry (0x80700008)
  BSS clear → $sp setup → j 0x807001E8
                              │
decompress_kernal_n_jump (0x807001E8)  ◄─┘
  printf("decompressing kernel:")
  │
  jal LZMA_decompress_engine_wrapper ──┐   $ra = 0x80700220
  $a0 = 0xA0000000 (uncached dst)      │
                                       ▼
  LZMA_decompress_engine_wrapper (0x80701458)
    printf("Uncompressing...")
    parse LZMA header (0x80702800)
    jal LZMA_decompress → 1.4MB → 5.22MB
    printf("done, booting the kernel.")
    lw $ra, 0xCC($sp)  ← $ra = 0x80700220
    jr $ra ─────────────────────────────┐
                                        │
decompress_kernal_n_jump + 0x38  ◄──────┘  (0x80700220)
  ... kernel jump code ...
  jr 0xA0000600 → eCos kernel entry point

LZMA (Lempel-Ziv-Markov chain Algorithm) Decompressor는 커널 코드를 KSEG1 (0xA0000000) 으로 압축 해제를 수행합니다.

Why KSEG1 for Kernel Decompression Output?


압축 해제된 커널은 0xA0000000 (KSEG1, 캐시되지 않음) 에 기록되며, 0x80000000 (KSEG0, 캐시됨) 에는 기록되지 않습니다. 두 주소 모두 동일한 물리적 DRAM 주소인 0x00000000에 매핑됩니다:

MIPS Virtual Address    Cache Mode    Physical Address
────────────────────    ──────────    ────────────────
0xA0000000 (KSEG1)       Uncached     0x00000000 (DRAM)
0x80000000 (KSEG0)        Cached      0x00000000 (DRAM)   <- 동일한 물리 주소

KSEG0 (0x80000000, 캐시됨) 에 데이터를 쓰면, 압축 해제된 데이터가 D-Cache에 저장되고, 최종적으로 DRAM에 다시 기록됩니다. 하지만 MIPS 아키텍처는 D-Cache와 I-Cache 간의 일관성을 유지하지 않기 때문에, I-Cache에는 여전히 오래된 명령어가 남아 있어 시스템 충돌이 발생할 수 있습니다.

KSEG1 (0xA0000000, 캐시되지 않음) 에 기록함으로써, 압축 해제된 데이터는 캐시를 거치지 않고 DRAM으로 직접 전송됩니다. 이후 CPU가 0x80000600 (KSEG0) 으로 점프하면 I-Cache miss가 발생하고 DRAM에서 최신 명령어를 올바르게 가져옵니다.

압축 해제가 완료되면, 부트로더는 제어권을 eCos 커널 진입점으로 넘기고 해당 진입점은 HAL을 초기화하고 스케줄러를 시작합니다.

eCos Firmware


이 시점에서 제어권이 부트로더에서 eCos 커널로 이전되며 이는 펌웨어 초기화에서 RTOS 실행으로의 전환을 의미합니다.

DRAM:80000600    eCos_exception_entry:
DRAM:80000600                    mfc0    $k0, Cause       # General Exception Vector: dispatch via table at 0x80000400. NOT the startup entry.
DRAM:80000604                    nop
DRAM:80000608                    andi    $k0, 0x7F
DRAM:8000060C                    li      $k1, dword_80000400
DRAM:80000614                    add     $k1, $k0
DRAM:80000618                    lw      $k1, 0($k1)
DRAM:8000061C                    nop
DRAM:80000620                    jr      $k1              # jump to 0x80000D00
DRAM:80000624                    nop
DRAM:80000624     # End of function eCos_exception_entry

첫 Entry에서 제어는 0x80000D00 주소의 리셋 핸들러로 전달됩니다. 이때 예외 벡터 메커니즘은 Cause 레지스터를 사용하지만, 이 시점에는 벡터 테이블이 아직 초기화되지 않았으므로 실행은 리셋 진입점으로 직접 진행됩니다.

C++ Static Constructors: Thread Registration

// Analyzed by Claude Code Opus 4.6

hal_reset_entry @ 0x80000D00:
DRAM:80000D0C  li      $k0, 0x80215A44
DRAM:80000D20  lui     $at, 0xA000
DRAM:80000D24  or      $k0, $at            
DRAM:80000D28  jalr    $k0                 
DRAM:80000D30  li      $gp, 0x8053FFE0     
DRAM:80000D38  li      $sp, 0x805405C0     
DRAM:80000D44  li      $a0, 16             ; Initalize exception vector table
DRAM:80000D48  li      $a1, 0x80000840     ; default_exception_handler
DRAM:80000D50  li      $a2, 0x80000400     ; vector table start address
loop:
DRAM:80000D58  sw      $a1, 0($a2)
DRAM:80000D5C  addi    $a2, 4
DRAM:80000D60  addi    $a0, -1
DRAM:80000D64  bnez    $a0, loop
DRAM:80000D7C  sw      0x80000948, 0x00($a0)   ; vector[0] = interrupt handler
DRAM:80000D88  sw      0x80000DFC, 0x24($a0)   ; vector[9] = breakpoint handler
DRAM:80000DA8  jal     hal_platform_init       ; SoC register
DRAM:80000DB0  jal     hal_variant_init        
; C++ static constructor: register application threads
DRAM:80000DB8  jal     cyg_hal_invoke_constructors
; Start scheduler(no return)
DRAM:80000DC0  j       cyg_scheduler_start

hal_reset_entry 함수 (0x80000D00) 는 예외 벡터 테이블 (0x80000400) 과 SoC 레지스터를 초기화합니다. 그 후 cyg_hal_invoke_constructors를 호출하여 애플리케이션 스레드를 등록합니다.

cyg_hal_invoke_constructors는 링커가 생성한 함수 포인터 배열인 .ctors 섹션을 순회합니다. 각 포인터는 main 함수 이전에 실행되는 C++ 정적 생성자입니다.

typedef void (*ctor_func)(void);
extern ctor_func __CTOR_LIST__[];

void cyg_hal_invoke_constructors() {
    for (ctor_func *p = &__CTOR_LIST__[1]; *p != NULL; p++)
        (*p)();   // call each constructor
}

eCos에서 스레드 생성은 애플리케이션 구조에 따라 cyg_thread_create를 통해 명시적으로 수행하거나 정적 생성자를 통해 암시적으로 수행할 수 있습니다. 많은 eCos 애플리케이션에서 이러한 생성자는 아래와 같이 cyg_thread_create를 호출하여 스케줄러에 스레드를 등록합니다:

void __static_init_xxx_main() {
    cyg_thread_create(
        PRIORITY,               // scheduling priority
        xxx_app_main,           // entry function (0x800xxxxx)
        0,                      // entry argument
        "xxx",                  // thread name
        stack_base,             // stack memory
        STACK_SIZE,             // stack size
        &thread_handle,         // output: handle
        &thread_obj             // output: thread object (HEAP)
    );
    cyg_thread_resume(thread_handle);
}

Scheduler Dispatch: Indirect Call to Application Thread

모든 생성자가 실행되고 스레드가 등록되면, cyg_scheduler_start는 스케줄링 루프에 진입합니다.

// Analyzed by Claude Code Opus 4.6
cyg_scheduler_start @ 0x8021DA5C:

DRAM:8021DA50  lw      $s0, 0x18($s1)       ; $s0 = highest-priority thread struct (from run queue)
DRAM:8021DA5C  di                           ; disable interrupts
DRAM:8021DA70  jal     sched_lock           ; lock scheduler
DRAM:8021DA7C  lw      $t8, 0x14($s0)       ; $t8 = thread->entry (function pointer)
DRAM:8021DA80  j       context_switch       ; switch to thread context
DRAM:8021DA84  lw      $a0, 0x00($s0)       ; $a0 = thread->arg (delay slot)

eCos는 우선순위 기반 선점형 스케줄러를 사용하며 실행 가능한 스레드 중 가장 우선순위가 높은 스레드가 항상 선택되어 실행됩니다. 스케줄러는 실행 큐에서 스레드 구조체를 가져와 진입 함수 포인터 (오프셋 0x14) 를 추출한 후 컨텍스트 스위치를 수행하여 제어권을 넘깁니다.

Conclusion


본 분석에서는 eCos 기반 임베디드 시스템의 부팅 과정을 플래시 메모리 추출부터 RTOS 수준 실행까지 전체적으로 살펴보았습니다.

시스템이 다음과 같이 여러 단계를 거쳐 부팅 및 초기화되는 과정을 분석하였습니다.

  • Stage 1 Boot ROM 코드: XIP 실행 및 최소 하드웨어 설정
  • Stage 1.5 부트로더: Gzip 압축 해제 및 캐시 동기화
  • Stage 2 부트로더: 하드웨어 초기화 및 펌웨어 검증
  • Kernel Decompressor: LZMA 압축 해제 후 캐시되지 않은 메모리로 이동 (KSEG1)
  • eCos 커널: HAL 초기화, 생성자 호출 및 스케줄러 시작

이러한 분석을 통해 임베디드 시스템에서는 부트로더와 RTOS 초기화 과정이 밀접하게 연관되어 있으며, 단계적으로 압축을 해제하여 실행하는 방식을 통해 플래시 메모리 사용량을 줄이는 기법이 적용되어 있음을 확인할 수 있었습니다.

Prev
2026-04-08 06:29:04