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

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


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

- 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 플래시의 최대 클럭 주파수 범위 내에 있으므로 별도의 속도 조정 없이 플래시 메모리를 읽을 수 있었습니다.


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


바이너리의 MD5 해시는 모두 동일했습니다. 펌웨어에는 gzip으로 압축된 데이터와 LZMA로 압축된 리소스가 포함되어 있습니다. LZMA로 압축된 리소스는 부팅 시퀀스에서 RAM으로 압축 해제됩니다. 펌웨어 정적 분석을 수행하기 전에 올바른 메모리 주소에 매핑된 세그먼트를 생성하기 위해서 SoC의 메모리 맵을 식별해야 합니다.
그래서 EMI (Electromagnetic Interference) 차폐막을 제거하고 SoC 모델을 확인했습니다. 해당 SoC는 MIPS 아키텍처 기반의 RTL8197F 제품군에 속합니다.


Boot Sequence Overview
Boot Stage 1

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


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

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로 압축된 이미지임을 나타냅니다.

또한, 매직 바이트가 “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 초기화 과정이 밀접하게 연관되어 있으며, 단계적으로 압축을 해제하여 실행하는 방식을 통해 플래시 메모리 사용량을 줄이는 기법이 적용되어 있음을 확인할 수 있었습니다.