펌웨어 종류와 넘겨받는 레지스터
OpenSBI의 일반 펌웨어는 FW_DYNAMIC, FW_JUMP, FW_PAYLOAD 세 가지입니다. 모두 fw_base.S의 시작 코드를 공유하지만, 이전 단계의 인자 처리와 다음 단계 이미지를 준비하는 방법이 다릅니다. FW_JUMP는 빌드 때 정한 주소로 넘어가고, FW_PAYLOAD는 다음 단계 이미지를 포함합니다. FW_DYNAMIC은 다음 단계 주소·모드·옵션을 struct fw_dynamic_info에서 읽고, 다음 단계용 arg1은 a1에서 저장합니다.
| 레지스터 | 이전 단계가 넣는 값 |
|---|---|
| a0 | 이 펌웨어로 들어오는 hart의 ID |
| a1 | device tree blob 주소. 8바이트 정렬이어야 합니다. |
| a2 | FW_DYNAMIC에서는 struct fw_dynamic_info의 주소. RV64는 8바이트, RV32는 4바이트 정렬입니다. |
이 레지스터 규약은 v1.9와 고정 커밋 6ad246a에서 같습니다. 후자는 a1·a2 값을 이미지 헤더로 넘기고, a0를 각 hart의 mhartid에서 얻는 선택지를 추가합니다. 헤더의 flags가 켜진 인자만 바뀝니다. v1.9 펌웨어 문서 FW_DYNAMIC 문서
fw_boot_hart가 갈림길을 정합니다
v1.9의 _start(48번 줄)는 a0~a2를 s0~s2에 보관한 뒤 fw_boot_hart를 호출하고, 반환값을 a6에 둡니다. a6가 -1이면 _try_lottery로 갑니다. 특정 hart가 지정되면 나머지는 _wait_for_boot_hart로 분기하지만, 지정된 hart는 바로 다음의 _try_lottery로 이어집니다. 지정은 참여 hart를 제한할 뿐, lottery 코드를 생략하지 않습니다. fw_base.S _start
| 펌웨어 | fw_boot_hart 반환값 | 결과 |
|---|---|---|
| FW_JUMP, FW_PAYLOAD | 항상 -1 | 모든 hart가 lottery에 참여합니다. |
| FW_DYNAMIC, info version 2 | fw_dynamic_info.boot_hart | 지정된 hart만 lottery로 이어지고 나머지는 기다립니다. -1UL이면 진입한 hart들이 lottery에 참여합니다. |
| FW_DYNAMIC, info version 1 | -1 | lottery를 씁니다. |
FW_DYNAMIC의 fw_boot_hart는 magic 0x4942534f와 버전 상한 2를 검사합니다. 정상적인 version 1은 -1을 반환하고 version 2는 boot_hart를 읽으므로, 이 구현을 version 2 이상 모두 지원한다고 읽으면 안 됩니다. fw_dynamic.h 주석은 이전 단계와 FW_DYNAMIC의 링크 주소가 겹칠 때, 다른 hart가 이전 단계 코드를 실행하는 동안 relocation이 이를 덮어쓸 위험을 설명합니다. 이를 피하기 위해 이전 단계는 마지막으로 진입하는 hart를 boot_hart로 지정할 수 있습니다. fw_dynamic.S fw_boot_hart fw_dynamic.h boot_hart 주석 fw_jump.S fw_boot_hart
lottery를 이긴 hart가 하는 일
_try_lottery(58번 줄)는 A 또는 Zaamo가 정의된 빌드에서 amoswap.w로 _boot_lottery에 1을 쓰고 이전 값을 봅니다. 이전 값이 0인 hart만 진행하고 나머지는 기다립니다. 이 조건이 아니고 Zalrsc가 정의되어 있으면 lr.w·sc.w 루프를 사용합니다. sc.w 실패는 재시도하며, 성공 후 읽었던 이전 값이 0이 아니면 기다립니다. 아래는 두 구현을 원자적 획득으로 묶은 흐름 요약입니다. _try_lottery
_start: save a0..a2; preferred = fw_boot_hart() preferred == -1 -> _try_lottery a0 != preferred -> _wait_for_boot_hart a0 == preferred -> fall through to _try_lottery _try_lottery: atomic acquire (_boot_lottery, 1) old != 0 -> _wait_for_boot_hart R_RISCV_RELATIVE relocation; zero .bss; temporary trap/stack fw_save_info; fw_platform_init; scratch setup; optional FDT copy fence rw, rw; _boot_status = BOOT_HART_DONE; j _start_warm _wait_for_boot_hart: spin until _boot_status == BOOT_HART_DONE _start_warm: validate hart index; scratch/stack/trap -> sbi_init
| 단계 | 줄 | 의미 |
|---|---|---|
| relocation | 75–95 | 링크 시작 주소 FW_TEXT_START와 실제 로드 주소의 차이를 구해 __rela_dyn의 R_RISCV_RELATIVE 항목을 고칩니다. |
| 초기 준비 | 96–164 | 레지스터 초기화, .bss를 0으로 채움, 임시 trap 핸들러(_start_hang)와 임시 stack 설정, fw_save_info와 fw_platform_init 호출입니다. |
| _scratch_init | 165–254 | 모든 hart의 scratch에 펌웨어 범위, 다음 단계 인자·주소·모드, warm boot 주소(_start_warm), platform 포인터, 옵션을 씁니다. |
| FDT 복사 | 256–297 | 필요하면 이전 단계가 준 FDT를 다음 단계용 주소로 복사합니다. |
| 완료 표시 | 299–304 | fence rw, rw 뒤에 _boot_status에 BOOT_STATUS_BOOT_HART_DONE을 저장하고 _start_warm으로 갑니다. |
기다리는 hart는 _boot_status를 반복해서 읽습니다. div t2, t2, zero 세 줄은 주석상 버스 트래픽을 줄이기 위한 지연입니다. 완료 값이 보이면 _start_warm(317번 줄)으로 이어집니다. 이 경로는 hart index를 확인하고, 유효하지 않으면 _start_hang으로 보냅니다. 유효한 hart는 자신의 scratch와 stack, trap 핸들러를 설정해 385번 줄에서 sbi_init을 호출합니다. 초기화를 담당한 hart도 이 공통 진입 경로를 사용합니다. sbi_init 내부의 이후 동작까지 모든 hart가 같다는 뜻은 아닙니다. 완료 표시와 대기 루프 _start_warm에서 sbi_init까지
master의 128바이트 헤더
고정 커밋 6ad246a의 펌웨어 이미지는 앞에 128바이트 헤더를 두고, flags로 a0~a2를 선택적으로 덮어씁니다. 첫 word는 헤더를 건너뛰는 4바이트 j 명령이므로 이전 단계의 진입 주소는 이미지 첫 바이트로 유지됩니다. norvc는 조립기의 압축 명령 선택을 막고, norelax는 링커가 이 점프를 나중에 압축하지 못하게 합니다. 헤더 구현 인자 override 구현
| Offset | 크기 | 내용 |
|---|---|---|
| 0x00 | 4 | 헤더를 건너뛰는 j 명령 |
| 0x04 | 4 | magic 'OSBI'(0x4942534f) |
| 0x08 | 4 | 헤더 version, 현재 1 |
| 0x0c | 4 | 빌드 XLEN, 32 또는 64 |
| 0x10 | 4 | _fw_end - _fw_start. .bss를 포함한 메모리 크기이며 flat binary 파일 크기가 아닙니다. |
| 0x14 | 4 | flags: bit 0 A0, bit 1 A1, bit 2 A2 override |
| 0x18 | 8 | a1 override 값(DTB 주소) |
| 0x28 | 8 | a2 override 값(fw_dynamic_info 주소) |
| 0x20, 0x30, 0x38 | 8, 8, 72 | 예약 영역. 앞의 두 슬롯은 향후 RV128 확장을 위한 상위 절반 자리입니다. |
a0용 값 칸은 없습니다. 헤더는 모든 hart가 공유하는 메모리이므로 hart마다 다른 ID를 담을 수 없기 때문입니다. 대신 A0 flag가 켜지면 _start_real이 각 hart에서 csrr a0, mhartid를 실행합니다. 새로 빌드한 이미지의 flags는 0이므로, 이미 레지스터로 값을 넘기는 이전 단계는 헤더를 몰라도 동작이 같습니다. 문서가 드는 사용 예는 별도 부팅 프로세서가 DRAM의 이미지를 패치하고 application hart를 풀어 주지만 그 hart의 레지스터는 직접 설정할 수 없는 구성입니다. 이 경우 fw_dynamic_info는 OpenSBI의 .bss나 펌웨어 위쪽 scratch 영역이 아닌 메모리에 두어야 합니다. master fw_header.md master _start와 _start_real
헤더의 모든 필드는 little-endian입니다. a1·a2 슬롯은 RV32에서도 8바이트지만 하위 4바이트만 읽고 상위 4바이트는 0이어야 합니다. _fw_end - _fw_start는 .bss까지 포함하며, 그 뒤의 scratch 등 추가 공간까지 모두 예약해 주는 값은 아닙니다. fw_dynamic_info는 OpenSBI가 덮어쓰지 않는 별도 메모리에 두어야 합니다.
v1.9 분석을 master에 옮길 때
| 항목 | v1.9 | master 6ad246a |
|---|---|---|
| 이미지 첫 명령 | _start의 레지스터 보관(MOV_3R) | 헤더를 건너뛰는 j, 실제 시작은 _start_real(111번 줄) |
| a0~a2의 출처 | 이전 단계의 레지스터 | 기본은 레지스터, flags가 켜진 항목만 헤더나 mhartid |
| lottery와 대기 | 58번, 307번 줄 | 같은 코드가 152번, 401번 줄로 이동 |
| sbi_init 호출 | 385번 줄 | 479번 줄 |
이 글의 기준은 v1.9가 가리키는 cbf9f673 커밋과 헤더가 있는 6ad246a 커밋입니다. 현재 master나 보드 공급자의 펌웨어가 이 코드와 같다고 가정하지 않습니다. 첫 j와 OSBI magic은 헤더 식별 단서이며, 값을 패치하기 전에는 고정 레이아웃의 version·XLEN·크기와 메모리 배치를 함께 확인해야 합니다. 줄 번호는 직접 받은 원본 파일 기준입니다. 여기서는 코드와 문서를 대조했으며 보드 실행·펌웨어 빌드 검증은 하지 않았습니다. OpenSBI v1.9 릴리스 master lottery에서 sbi_init까지