Search

부트로더란?

Category
임베디드
study
Column
2023/01/16 11:19
Tags
study
**틀린 내용이 있다면 언제든 댓글 달아주세요

1. 개요

부트로더의 사전적 정의는 다음과 같다.
부트로더(boot loader)란 운영 체제가 시동되기 이전에 미리 실행되면서 커널이 올바르게 시동되기 위해 필요한 모든 관련 작업을 마무리하고. 최종적으로 운영 체제를 시동시키기 위한 목적을 가진 프로그램을 말한다.
즉 임베디드 보드에 전원이 인가되었을 때, 메모리, 하드웨어(네트워크, 프로세서 속도, 인터럽트), 코드/데이터/스택 영역의 설정 및 초기화를 진행하며 커널을 메모리에 적재하여 커널을 실행시키는 역할을 한다.
임베디드 시스템에서 커널이 로딩되고 실행되는 대략 적인 과정은 위 그림과 같다. 이번 학습의 목표는 부트로더의 자세한 구조와 실제 커널이 로딩되기 전까지의 과정 하드웨어와 소프트웨어 두 개의 관점에서 좀 더 자세히 살펴보며 최종적으로 u-boot의 소스코드를 통해 학습한 내용을 코드로 확인해보고자 한다.

2. 부팅 절차

실제 컬퀌, 삼성 등 여러 chip 제조사들에서 제작하는 칩 내부에 초기 부팅 과정을 수행하는 코드가 담긴다. cpu 칩 내부에서 초기 과정을 거쳐 실제 end user단에서 보이는 uboot라던지 등의 우리가 익숙한 부트로더가 수행되는 것이다. 위 사진에서 secondary bootloader라고 부르는 것같다.
위 그림의 primary bootloader, secondary bootloader 부분을 좀더 자세히 살펴보자.
부트로더는 실제 bl0, bl1, bl2 총 3가지의 단계로 구성되며 이에 맞게 동작한다.
=============================
BL2
=============================
BL1
=============================
BL0
=============================
BL0와 BL1은 chip vendor 사에서 제작 및 release하는 코드 영역이다. 대표적으로 Qualcomm, Samsung 등이 있으며 해당 벤더사에서 만드는 CPU가 동작하기 위한 기본 영역이라고 보면 된다.
iROM : CPU 내부에 존재하는 ROM 영역 iRAM : CPU 내부에 존재하는 RAM 영역 // SRAM이라고 보면 될듯 OM : Operation Mode으로 부팅 모드를 뜻한다.
BL0(boot-rom or pre-boot loader))
iROM에 저장된 코드 영역 - ROM Code
크게 두 가지 작업 수행
디바이스 환경설정과 프라이머리 페리페럴의 초기화
다음 부트로더(SPL)를 위한 디바이스 준비
OM pin으로부터 부팅 매체를 찾아 BL1을 iRAM(SRAM)으로 읽어들인다. (OM pin은 OneNAND, NAND, MoviNAND, eSSD and iNAND와 같은 booting device를 결정한다)
이후 제어권을 BL1으로 넘김
BL0은 칩 제조사가 생산시점에서 기록하게되며 삼성 휴대폰의 경우 삼성 반도체가 역할을 맡는다.(추정) 따라서 개발자가 관연하는 부분이 아니다.
BL1(first boot loader, SPL..?)
일반적으로 SRAM이 U-Boot와 같은 전체 부트로더를 로드하기에 충분히 크지 않기 때문에 필요한 단계
booting device(부팅 매체)처럼 외부 메모리에 저장되어있는 코드 영역
NAND/MMC and DRAM controller 등의 초기화 진행
second boot loader 로딩
부팅 장치(nand 나 sd카드)에 저장된 BL2 를 읽어서 메모리로 로딩&실행
이후 제어권을 BL2로 넘김
BL1은 2차벤더가 기록하게되며 삼성 휴대폰의 경우삼성 모바일 사업부, ODROID의 경우 HARDKERNEL에서 책임을 담당(추정)
BL2
BL2는 일반적으로 u-boot이나 BLOB, grub 과 같은 널리 알려진 Boot Loader가 사용됨
cpu, clock, uart, memory 등의 중요한 초기화 작업 수행
커널을 dram에 올려 os loading 수행
커널로 제어권 넘김
BL0, BL1, BL2 등과 같은 자세한 부팅 흐름을 공부하면서 참고하는 자료마다 조금씩 설명 부분이 달라서 매우 헷갈렸지만 이론적인 부분은 위에서 설명한 흐름과 동일한 것 같다. 실제 참고했던 자료들에서 위 학습 내용을 적용시켜 다시 확인해보자.

2.1 참고자료1

부팅흐름
1.
시스템이 리셋되면 iROM부터 PC가 시작된다.
BL0 을 수행하는 단계이다. OM pin으로부터 부팅매체를 찾아 디바이스로부터 BL1을 iSRAM으로 읽어들인다.
2.
제어권을 넘겨받은 BL1은 BL2를 로딩한다
BL1를 수행하는 단계로, 부팅 장치(nand 나 sd카드)에 저장된 BL2 를 읽어서 메모리로 로딩&실행한다.
3.
BL2는 일반적으로 u-boot나 BLOB, grub과 같은 널리 알려진 Boot-loader가 수행되는 단계이다.
BL2를 수행하는 단계로 초기화 작업을 수행한 뒤 커널을 메모리에 올려 로딩한다.
4.
해당 글은 ARM SECURE BOOT PROCEDURE 와 관련된 내용을 자세히 설명한다.

2.2 참고자료2

부팅 흐름
1.
ARM CPU의 전원이 켜지면 PC 레지스터가 IC에 내장된 rom 시작 부분을 가리킨다. 이 rom을 BROM(boot rom)이라 하며 부팅 장치의 pre-loader를 찾아 isram에 로드한다.
BL0 단계이며 isram에 로드하는 pre-loader가 bl1인것 같다
위 그림의 1-3 단계 설명(BL0)
2.
pre-loader가 dram을 초기화한 후 부트로더의 2단계 코드를 플래시(nand/emmc)에서 dram으로 로드하여 실행한다. 하드웨어 장치 초기화, 시스템 매모리 맵 확인 커널 매개변수 설정 등을 수행한다.
BL1를 수행하는 단계로, 부팅 장치(nand 나 sd카드)에 저장된 BL2 를 읽어서 메모리로 로딩&실행하는 과정이다.
3.
dram에 올라온 부팅 이미지(ramdisk, kernel 등)의 압축을 해제한 뒤 dram으로 로드하고 uboot같은 부트로더가 수행된다.
BL2를 수행하는 단계

2.3 참고자료3

부팅흐름
1.
1단계 : ROM 코드
부팅 1단계로 boot rom ROM 코드가 실행된다. 이후 sram으로 작은 부트코드를 로드한다. 예를 들면 NAND 플래시 메모리, SPI (Serial Peripheral Interface)를 통해 연결된 플래시 메모리, MMC 장치의 첫 번째 섹터 (SD 카드) 등에서 SPL을 읽는다
BL0 단계로 OM pin을 찾아 BL1을 isram에 올리는 과정이다. SPL이 BL1이라고 보면 될것 같다
2.
2단계 : SPL(Secondary Program Loader)
SPL은 sram에서는 전체 부트로더를 로드하기에 충분히 크지 않기 때문에 필요하다. SPL은 TPL을 DRAM에 로드하도록 메모리 컨트롤러 및 필수 부분의 초기화를 수행한다. uboot.bin 과 같은 알려진 파일 이름을 사용하여 프로그램을 메모리에 올린다.
BL1 단계로 BL2를 메모리에 올리는 과정이다.
3.
3단계 : TPL(Tertiary Program Loader)
U-Boot 또는 Barebox와 같은 전체 부트 로더는 간단한 명령 줄 사용자 인터페이스, 커널 이미지를 네트웍 또는 플래시 저장소에서 로드 및 부팅과 같은 유지 관리 작업을 수행한다. 커널이 실행되면 부트로더는 일반적으로 메모리에서 사라지고 시스템 작동에 더 이상 참여하지 않는다.
BL2 단계로 최종적으로 커널이 로딩되면 그 이후 제어권은 커널이 갖게 된다.

2.4 참고자료4

해당 자료는 부팅 과정에 대한 개괄적인 내용부터 잘 나와있다. 전원이 인가되면 ROM code가 실행되며 부팅 디바이스를 찾아 우리가 아는 uboot 같은 부트로더가 최종 실행되고 최종적으로 kernel및 platform이 실행된다.
pre-bootloader가 bl0 단계로 ROM 코드가 실행되면 BL1인 First Stage Bootloader를 isram에 로드한다. First stage bootloader는 dram 컨트롤러 초기화 및 부팅 장치를 찾아 BL2 즉 second stage boot loader를 램에 올린다. 최종적으로 second stage boot loader에서 초기화 및 커널을 로딩한다.
Samsung Exynos4412 chip의 부팅과정은 위와 같다.
부팅과정
1.
BL0 : iROM code (boot-rom or pre-boot loader)
bootrom 즉 irom에 존재하는 코드가 제일 먼저 실행된다. 이후 부팅 매체(nand or mmc or sd 등)를 찾아 bl1을 isram에 로드한다.
2.
BL1 : first boot loader
nand/mmc, dram 컨트롤러를 초기화 하고 second boot loader 즉 bl2를 메모리에 로드한다.
3.
BL2 : second boot loader
하드웨어 초기화 및 uboot 같은 우리가 아는 부트로더가 실행되며 최종적으로 커널이 메모리에서 부팅되며 제어권을 커널로 넘긴다.

2.5 참고자료5

임베디드 시스템에서의 부팅과정을 다음과 같다
부팅과정
1.
위 그림에서는 BL0 과정은 생략된 것으로 보인다. 제일 처음에는 BL0가 수행되면서 i-ROM 코드가 수행된다. 위 그림에서는 MBR 영역이 BL0라고 생각하면 된다.(틀리면 댓글 남겨주세요).
2.
그 후BL0에서 OM 를 찾아 BL1을 iRAM에 로드한다
3.
BL1은 BL2를 찾아 메모리에 로드한다.
4.
BL2에서는 boot image를 메모리에 로드하고 커널을 로딩한다
5.
제어권을 커널로 넘어와 커널 로딩이 시작된다.

3. u-boot 구조 분석

실제 우리가 아는 uboot 같은 부트로더가 실행되기 전까지 어떠한 과정을 거쳐서 부팅이 진행되는지 살펴보았다. 이제는 본론으로 들어가 여러 임베디드에 사용되는 부트로더 중 uboot를 자세히 분석하고자 한다.
우선 u-boot의 디렉토리 구조를 보면 다음과 같다.
arch
arm, mips, powerpc 등 프로세서 아키텍처별로 서로 다른 디렉터리를 구성하고 있다. 각 아키텍처에 따른 소스를 구성하는 디렉토리. start.S가 존재
board
board와 관련된 코드. 보통 제조사가 만든 보드에 대한 코드가 존재
common
u-boot에서 공통적으로 사용되는 소스를 구현한 디렉토리
disk
디스크 드라이버와 파티션 관련 코드 존재
drivers
gpio, i2c, pci, serial, usb등 외부 장치의 드라이버 관련 코드 존재
fs
u-boot에서 지원하는 다양한 파일시스템에 대한 소스가 존재
lib
모든 아키텍처와 관련된 라이브러리
net
네트워크 관련 코드
post
power on self test 관련 코드
u-boot의 전체 부팅 시퀀스는 위 그림과 같다. 초기에 모든 인터럽트를 disable 시킨뒤 ROM의 있는 데이터를 RAM으로 복사한다.
그 다음 초기화 되지 않는 영역을 초기화 시키고 스택 초기화를 진행한다. 그 다음 힙 영역과 global 영역들의 초기화 작업을 진행 한뒤 마지막으로 모든 인터럽트를 다시 enable 한뒤 main_loop으로 이동한다. 결국 다 초기화와 관련된 작업인 것을 알 수 있다. 어떤 초기화를 수행하는지 좀더 자세히 살펴보자.
1.
Reset CPU and Hardware
start.S 파일은 uboot의 entry point이다. start.S에서는 제일 처음 여러가지 인터럽트와 예외를 처리할수 있는 vector table을 설정한다.
.globl _start _start: b reset // reset 함수로 분기 ldr pc, _undefined_instruction // ldr pc, _software_interrupt // ldr pc, _prefetch_abort // ldr pc, _data_abort // 예외 상황에 대한 처리 handler table ldr pc, _not_used // ldr pc, _irq // ldr pc, _fiq //
Assembly
복사
2.
Disable IRQ & FIQ
start.S 코드에 보면 reset 함수로 제일 처음 분기를 한다. 해당 작업을 의미하는 것으로 보인다.
reset: /* * set the cpu to SVC32 mode */ mrs r0,cpsr bic r0,r0,#0x1f orr r0,r0,#0x13 msr cpsr,r0 ...
Assembly
복사
reset 함수는 주로 다음의 작업을 수행한다
save_boot_pararm
disable FIQ & IRQ
SVC 모드로 전환
cpu_init_crit 함수 호출 // in lowlevel_init.S
3.
Low Level Initialization
cpu_init_crit 수행
I,D Cache 무효화
MMU Disable
lowlevel_init() 함수 호출
lowlevel_init() - 하드웨어 초기화 역할
uart, PMIC, 메모리 등 초기화
board_init_f 호출
4.
Setup SP for Early Board Setup Envritoment(ASM→C)
crt0.S에 존재하는 _main 함수 호출
setup inital stack & global data
and jump to C routine for board init
_main 내부의 board_init_f 함수 호출
board_init_f 내부에서 uboot의 text code를 memory에 옮김
기본적인 초기화 수행 이후 현재 코드가 실행되고 있는 위치와 실제 u-boot가 위치해야하는 주소를 비교하여 relocation 여부를 결장한다. 만약 현재 위치가 u-boot의 실제 실행 위치와 다르면 스스로를 실행위치(dram)으로 복사한다.
start.S의 relocate_code 함수 호출
... #ifndef CONFIG_SKIP_LOWLEVEL_INIT bl cpu_init_crit #endif #ifdef CONFIG_LPC2292 bl lowlevel_init #endif #ifndef CONFIG_SKIP_RELOCATE_UBOOT relocate: /* relocate U-Boot to RAM */ adr r0, _start /* r0 <- current position of code */ ldr r1, _TEXT_BASE /* test if we run from flash or RAM */ cmp r0, r1 /* don't reloc during debug */ ...
Assembly
복사
5.
General Relocation
start.S의 relocate_code() 함수 실행
램으로 코드 재배치
BSS 영역 초기화
relocate 내부에서 board_init_r 실행
6.
Setup GD and JUMP to FInal Board Setup
board_init_r 실행
내부 main loop 실행
7.
전체적인 흐름 요약

4. u-boot 소스코드 분석

wget ftp://ftp.denx.de/pub/u-boot/u-boot-2010.03.tar.bz2
Assembly
복사
해당 버전을 기준으로 실제 소스코드 분석을 해보자
/cpu/arm926ejs/start.S
/* * armboot - Startup Code for ARM926EJS CPU-core */ #include <config.h> #include <common.h> #include <version.h> #if defined(CONFIG_OMAP1610) #include <./configs/omap1510.h> #elif defined(CONFIG_OMAP730) #include <./configs/omap730.h> #endif /* ************************************************************************* * * Jump vector table as in table 3.1 in [1] * ************************************************************************* */ .globl _start _start: b reset // reset 함수로 분기 #ifdef CONFIG_PRELOADER /* No exception handlers in preloader */ ldr pc, _hang ldr pc, _hang ldr pc, _hang ldr pc, _hang ldr pc, _hang ldr pc, _hang ldr pc, _hang _hang: .word do_hang /* pad to 64 byte boundary */ .word 0x12345678 .word 0x12345678 .word 0x12345678 .word 0x12345678 .word 0x12345678 .word 0x12345678 .word 0x12345678 #else //exception vector 설정 ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq ldr pc, _fiq //exception vector의 위치 정의 _undefined_instruction: .word undefined_instruction _software_interrupt: .word software_interrupt _prefetch_abort: .word prefetch_abort _data_abort: .word data_abort _not_used: .word not_used _irq: .word irq _fiq: .word fiq #endif /* CONFIG_PRELOADER */ .balignl 16,0xdeadbeef /* ************************************************************************* * * Startup Code (reset vector) * * do important init only if we don't start from memory! * setup Memory and board specific bits prior to relocation. * relocate armboot to ram * setup stack * ************************************************************************* */ _TEXT_BASE: .word TEXT_BASE // RAM에 uboot가 올라가는 주소. Config.mk에 정의되어 있음 // board/armltd/versatile/Config.mk -> TEXT_BASE = 0x01000000 .globl _armboot_start _armboot_start: .word _start //_armboot_start의 address에 _start의 address 저장 /* * These are defined in the board-specific linker script. */ .globl _bss_start // u-boot.lds를 참조하여 bss start, end 주소 설정 _bss_start: .word __bss_start .globl _bss_end _bss_end: .word _end #ifdef CONFIG_USE_IRQ /* IRQ stack memory (calculated at run-time) */ .globl IRQ_STACK_START IRQ_STACK_START: .word 0x0badc0de /* IRQ stack memory (calculated at run-time) */ .globl FIQ_STACK_START FIQ_STACK_START: .word 0x0badc0de #endif /* * the actual reset code */ reset: // start.S에서 제일 처음 분기하는 reset 함수 /* * set the cpu to SVC32 mode */ //arm 동작 모드를 svc(supervisor mode) 모드로 변경 mrs r0,cpsr bic r0,r0,#0x1f orr r0,r0,#0xd3 msr cpsr,r0 /* * we do sys-critical inits only at reboot, * not when booting from ram! */ #ifndef CONFIG_SKIP_LOWLEVEL_INIT // cpu_init_cirt로 분기 // 여기서 MMU를 disable 한뒤 bl cpu_init_crit #endif #ifndef CONFIG_SKIP_RELOCATE_UBOOT //flash memory에서 self 코드(armboot)를 sdram으로 복사 relocate: /* relocate U-Boot to RAM */ adr r0, _start /* r0 <- current position of code */ //_start의 위치를 r0에 저장 ldr r1, _TEXT_BASE /* test if we run from flash or RAM */ cmp r0, r1 /* don't reloc during debug */ // r0와 r1이 같다면 beq stack_setup // stack_setup 수행 ldr r2, _armboot_start // _armboot_start 주소를 r2에 저장 ldr r3, _bss_start //bss_start 위치를 r3에 저장. sub r2, r3, r2 /* r2 <- size of armboot */ add r2, r0, r2 /* r2 <- source end address */ // r3-r2 -> r2 : armboot 크기 // r0+r2 -> r2 : source end 주소 copy_loop: ldmia r0!, {r3-r10} /* copy from source address [r0] */ // r0 위치에서 차례로 데이터를 읽어 r3-r10에 저장 후 r0=r0+12 수행 stmia r1!, {r3-r10} /* copy to target address [r1] */ // r1이 가리키는 address에 r3-r10의 데이터를 저장 cmp r0, r2 /* until source end addreee [r2] */ //r0가 end address인 r2에 동일하면 루프 종료 ble copy_loop #endif /* CONFIG_SKIP_RELOCATE_UBOOT */ /* Set up the stack // stack 설정. stack 초기화 이후부터 c코드 사용 가능 */ stack_setup: ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */ sub sp, r0, #128 /* leave 32 words for abort-stack */ #ifndef CONFIG_PRELOADER sub r0, r0, #CONFIG_SYS_MALLOC_LEN /* malloc area */ sub r0, r0, #CONFIG_SYS_GBL_DATA_SIZE /* bdinfo */ #ifdef CONFIG_USE_IRQ sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) #endif #endif /* CONFIG_PRELOADER */ sub sp, r0, #12 /* leave 3 words for abort-stack */ bic sp, r0, #7 /* 8-byte align stack for ABI compliance */ // r0 - 12 = sp로 저장. sp의 최상단 3 word를 비워두는 이유는 // abort exception이 발생했을 때 exception이 발생하기 전의 pc값과 cpsr 값을 저장하여 debug 정보에 사용하기 위해 clear_bss: // bss 영역 초기화 ldr r0, _bss_start /* find start of bss segment */ ldr r1, _bss_end /* stop here */ mov r2, #0x00000000 /* clear */ #ifndef CONFIG_PRELOADER clbss_l:str r2, [r0] /* clear loop... */ add r0, r0, #4 cmp r0, r1 ble clbss_l bl coloured_LED_init bl red_LED_on #endif /* CONFIG_PRELOADER */ ldr pc, _start_armboot // bss 초기화 후 _start_armboot로 분기 // 이후 부터는 RAM에 복사된 코드 수행 // board_init, Interrupt_init, env_init, init_baudrate 등의 // init 함수 호인터를 순차적으로 call // start_armboot부터는 c 함수이다.(stack이 초기화 되었기 때문에 가능) _start_armboot: #ifdef CONFIG_NAND_SPL // nand spl로 설정되어 있다면 nand_boot 실행 .word nand_boot #else // 아니면 start_armboot 실행 .word start_armboot #endif /* CONFIG_NAND_SPL */ ...
Assembly
복사
위 과정까지 요약하면 다음과 같다.
소스코드 상의 cpu는 다르지만 흐름만 보면 이해하기 쉽게 diagram으로 표현되어있다.
/lib_arm/board.c 위쪽 코드(start_armboot 함수 전)
/* * To match the U-Boot user interface on ARM platforms to the U-Boot * standard (as on PPC platforms), some messages with debug character * are removed from the default U-Boot build. * * Define DEBUG here if you want additional info as shown below * printed upon startup: * * U-Boot code: 00F00000 -> 00F3C774 BSS: -> 00FC3274 * IRQ Stack: 00ebff7c * FIQ Stack: 00ebef7c */ #include <common.h> #include <command.h> #include <malloc.h> #include <stdio_dev.h> #include <timestamp.h> #include <version.h> #include <net.h> #include <serial.h> #include <nand.h> #include <onenand_uboot.h> #include <mmc.h> #ifdef CONFIG_BITBANGMII #include <miiphy.h> #endif #ifdef CONFIG_DRIVER_SMC91111 #include "../drivers/net/smc91111.h" #endif #ifdef CONFIG_DRIVER_LAN91C96 #include "../drivers/net/lan91c96.h" #endif DECLARE_GLOBAL_DATA_PTR; ulong monitor_flash_len; #ifdef CONFIG_HAS_DATAFLASH extern int AT91F_DataflashInit(void); extern void dataflash_print_info(void); #endif #ifndef CONFIG_IDENT_STRING #define CONFIG_IDENT_STRING "" #endif const char version_string[] = U_BOOT_VERSION" (" U_BOOT_DATE " - " U_BOOT_TIME ")"CONFIG_IDENT_STRING; #ifdef CONFIG_DRIVER_RTL8019 extern void rtl8019_get_enetaddr (uchar * addr); #endif #if defined(CONFIG_HARD_I2C) || \ defined(CONFIG_SOFT_I2C) #include <i2c.h> #endif /************************************************************************ * Coloured LED functionality ************************************************************************ * May be supplied by boards if desired */ void inline __coloured_LED_init (void) {} void coloured_LED_init (void) __attribute__((weak, alias("__coloured_LED_init"))); void inline __red_LED_on (void) {} void red_LED_on (void) __attribute__((weak, alias("__red_LED_on"))); void inline __red_LED_off(void) {} void red_LED_off(void) __attribute__((weak, alias("__red_LED_off"))); void inline __green_LED_on(void) {} void green_LED_on(void) __attribute__((weak, alias("__green_LED_on"))); void inline __green_LED_off(void) {} void green_LED_off(void) __attribute__((weak, alias("__green_LED_off"))); void inline __yellow_LED_on(void) {} void yellow_LED_on(void) __attribute__((weak, alias("__yellow_LED_on"))); void inline __yellow_LED_off(void) {} void yellow_LED_off(void) __attribute__((weak, alias("__yellow_LED_off"))); void inline __blue_LED_on(void) {} void blue_LED_on(void) __attribute__((weak, alias("__blue_LED_on"))); void inline __blue_LED_off(void) {} void blue_LED_off(void) __attribute__((weak, alias("__blue_LED_off"))); /************************************************************************ * Init Utilities * ************************************************************************ * Some of this code should be moved into the core functions, * or dropped completely, * but let's get it working (again) first... */ #if defined(CONFIG_ARM_DCC) && !defined(CONFIG_BAUDRATE) #define CONFIG_BAUDRATE 115200 #endif static int init_baudrate (void) { char tmp[64]; /* long enough for environment variables */ int i = getenv_r ("baudrate", tmp, sizeof (tmp)); gd->bd->bi_baudrate = gd->baudrate = (i > 0) ? (int) simple_strtoul (tmp, NULL, 10) : CONFIG_BAUDRATE; return (0); } static int display_banner (void) { printf ("\n\n%s\n\n", version_string); debug ("U-Boot code: %08lX -> %08lX BSS: -> %08lX\n", _armboot_start, _bss_start, _bss_end); #ifdef CONFIG_MODEM_SUPPORT debug ("Modem Support enabled\n"); #endif #ifdef CONFIG_USE_IRQ debug ("IRQ Stack: %08lx\n", IRQ_STACK_START); debug ("FIQ Stack: %08lx\n", FIQ_STACK_START); #endif return (0); } /* * WARNING: this code looks "cleaner" than the PowerPC version, but * has the disadvantage that you either get nothing, or everything. * On PowerPC, you might see "DRAM: " before the system hangs - which * gives a simple yet clear indication which part of the * initialization if failing. */ static int display_dram_config (void) { int i; #ifdef DEBUG puts ("RAM Configuration:\n"); for(i=0; i<CONFIG_NR_DRAM_BANKS; i++) { printf ("Bank #%d: %08lx ", i, gd->bd->bi_dram[i].start); print_size (gd->bd->bi_dram[i].size, "\n"); } #else ulong size = 0; for (i=0; i<CONFIG_NR_DRAM_BANKS; i++) { size += gd->bd->bi_dram[i].size; } puts("DRAM: "); print_size(size, "\n"); #endif return (0); } #ifndef CONFIG_SYS_NO_FLASH static void display_flash_config (ulong size) { puts ("Flash: "); print_size (size, "\n"); } #endif /* CONFIG_SYS_NO_FLASH */ #if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C) static int init_func_i2c (void) { puts ("I2C: "); i2c_init (CONFIG_SYS_I2C_SPEED, CONFIG_SYS_I2C_SLAVE); puts ("ready\n"); return (0); } #endif #if defined(CONFIG_CMD_PCI) || defined (CONFIG_PCI) #include <pci.h> static int arm_pci_init(void) { pci_init(); return 0; } #endif /* CONFIG_CMD_PCI || CONFIG_PCI */ /* * Breathe some life into the board... * * Initialize a serial port as console, and carry out some hardware * tests. * * The first part of initialization is running from Flash memory; * its main purpose is to initialize the RAM so that we * can relocate the monitor code to RAM. */ /* * All attempts to come up with a "common" initialization sequence * that works for all boards and architectures failed: some of the * requirements are just _too_ different. To get rid of the resulting * mess of board dependent #ifdef'ed code we now make the whole * initialization sequence configurable to the user. * * The requirements for any new initalization function is simple: it * receives a pointer to the "global data" structure as it's only * argument, and returns an integer return code, where 0 means * "continue" and != 0 means "fatal error, hang the system". */ typedef int (init_fnc_t) (void); int print_cpuinfo (void); init_fnc_t *init_sequence[] = { #if defined(CONFIG_ARCH_CPU_INIT) arch_cpu_init, /* basic arch cpu dependent setup */ #endif board_init, /* basic board dependent setup */ #if defined(CONFIG_USE_IRQ) interrupt_init, /* set up exceptions */ #endif timer_init, /* initialize timer */ #ifdef CONFIG_FSL_ESDHC get_clocks, #endif env_init, /* initialize environment */ init_baudrate, /* initialze baudrate settings */ serial_init, /* serial communications setup */ console_init_f, /* stage 1 init of console */ display_banner, /* say that we are here */ #if defined(CONFIG_DISPLAY_CPUINFO) print_cpuinfo, /* display cpu info (and speed) */ #endif #if defined(CONFIG_DISPLAY_BOARDINFO) checkboard, /* display board info */ #endif #if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C) init_func_i2c, #endif dram_init, /* configure available RAM banks */ #if defined(CONFIG_CMD_PCI) || defined (CONFIG_PCI) arm_pci_init, #endif display_dram_config, NULL, };
C
복사
/lib_arm/board.c (start_armboot 함수부터)
void start_armboot (void) { init_fnc_t **init_fnc_ptr; // 보드의 장치 초기화를 위한 함수 포인터 배열 char *s; #if defined(CONFIG_VFD) || defined(CONFIG_LCD) unsigned long addr; #endif /* Pointer is writable since we allocated a register for it */ //global_data.h에 정의되어 있으며 baudrate, board specific(bd_t* bd), relocation offset, // environment variable address, fram-buffer addr, cpu clock, bus clock, ram size // reset status, jump table 등의 정보를 가지고 있다 gd = (gd_t*)(_armboot_start - CONFIG_SYS_MALLOC_LEN - sizeof(gd_t)); // gd 포인터 설정 /* compiler optimization barrier needed for GCC >= 3.4 */ __asm__ __volatile__("": : :"memory"); // 메모리 초기화(gd, bd 메모리를 clear) memset ((void*)gd, 0, sizeof (gd_t)); gd->bd = (bd_t*)((char*)gd - sizeof(bd_t)); memset (gd->bd, 0, sizeof (bd_t)); gd->flags |= GD_FLG_RELOC; // relocation flag 설정 monitor_flash_len = _bss_start - _armboot_start; // 루프를 돌면서 board에 관련된 모든 차원을 초기화 // 초기화는 init_sequence에 선언된 순서대로 진행 // 위에 init_sequence 정의되어 있음.(arch_cpu_init, board_init, 등) for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { if ((*init_fnc_ptr)() != 0) { // 에러 발생시 reset 메시지 출력 hang (); } } /* armboot_start is defined in the board-specific linker script */ // heap 영역 설정 mem_malloc_init (_armboot_start - CONFIG_SYS_MALLOC_LEN, CONFIG_SYS_MALLOC_LEN); #ifndef CONFIG_SYS_NO_FLASH /* configure available FLASH banks */ // flash를 초기화 하고 flash의 size를 console에 출력 display_flash_config (flash_init ()); #endif /* CONFIG_SYS_NO_FLASH */ #ifdef CONFIG_VFD # ifndef PAGE_SIZE # define PAGE_SIZE 4096 # endif /* * reserve memory for VFD display (always full pages) */ /* bss_end is defined in the board-specific linker script */ addr = (_bss_end + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1); vfd_setmem (addr); gd->fb_base = addr; #endif /* CONFIG_VFD */ #ifdef CONFIG_LCD /* board init may have inited fb_base */ if (!gd->fb_base) { # ifndef PAGE_SIZE # define PAGE_SIZE 4096 # endif /* * reserve memory for LCD display (always full pages) */ /* bss_end is defined in the board-specific linker script */ addr = (_bss_end + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1); lcd_setmem (addr); gd->fb_base = addr; } #endif /* CONFIG_LCD */ #if defined(CONFIG_CMD_NAND) puts ("NAND: "); nand_init(); /* go init the NAND */ #endif #if defined(CONFIG_CMD_ONENAND) onenand_init(); #endif #ifdef CONFIG_HAS_DATAFLASH AT91F_DataflashInit(); dataflash_print_info(); #endif /* initialize environment */ // flash 에서 sdram으로 복사 env_relocate (); #ifdef CONFIG_VFD /* must do this after the framebuffer is allocated */ drv_vfd_init(); #endif /* CONFIG_VFD */ #ifdef CONFIG_SERIAL_MULTI serial_initialize(); #endif /* IP Address */ gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr"); stdio_init (); /* get the devices list going. */ jumptable_init (); #if defined(CONFIG_API) /* Initialize API */ api_init (); #endif console_init_r (); /* fully init console as a device */ #if defined(CONFIG_ARCH_MISC_INIT) /* miscellaneous arch dependent initialisations */ arch_misc_init (); #endif #if defined(CONFIG_MISC_INIT_R) /* miscellaneous platform dependent initialisations */ misc_init_r (); #endif /* enable exceptions */ // 인터럽트 enable enable_interrupts (); /* Perform network card initialisation if necessary */ #ifdef CONFIG_DRIVER_TI_EMAC /* XXX: this needs to be moved to board init */ extern void davinci_eth_set_mac_addr (const u_int8_t *addr); if (getenv ("ethaddr")) { uchar enetaddr[6]; eth_getenv_enetaddr("ethaddr", enetaddr); davinci_eth_set_mac_addr(enetaddr); } #endif #if defined(CONFIG_DRIVER_SMC91111) || defined (CONFIG_DRIVER_LAN91C96) /* XXX: this needs to be moved to board init */ if (getenv ("ethaddr")) { uchar enetaddr[6]; eth_getenv_enetaddr("ethaddr", enetaddr); smc_set_mac_addr(enetaddr); } #endif /* CONFIG_DRIVER_SMC91111 || CONFIG_DRIVER_LAN91C96 */ /* Initialize from environment */ if ((s = getenv ("loadaddr")) != NULL) { load_addr = simple_strtoul (s, NULL, 16); } #if defined(CONFIG_CMD_NET) if ((s = getenv ("bootfile")) != NULL) { copy_filename (BootFile, s, sizeof (BootFile)); } #endif #ifdef BOARD_LATE_INIT board_late_init (); #endif #ifdef CONFIG_GENERIC_MMC puts ("MMC: "); mmc_initialize (gd->bd); #endif #ifdef CONFIG_BITBANGMII bb_miiphy_init(); #endif #if defined(CONFIG_CMD_NET) #if defined(CONFIG_NET_MULTI) puts ("Net: "); #endif eth_initialize(gd->bd); #if defined(CONFIG_RESET_PHY_R) debug ("Reset Ethernet PHY\n"); reset_phy(); #endif #endif /* main_loop() can return to retry autoboot, if so just run it again. */ for (;;) { main_loop (); // main loop 진입 } /* NOTREACHED - no way out of command loop except booting */ } void hang (void) { puts ("### ERROR ### Please RESET the board ###\n"); for (;;); }
C
복사
start_armboot의 주요 함수 로직을 요약하면 다음과 같다.
보드 초기화 후 main_loop 호출
전체 흐름을 요약하면 다음과 같다.
마지막으로 main_loop 함수를 살펴보자.
common/main.c
... void main_loop (void) { #ifndef CONFIG_SYS_HUSH_PARSER static char lastcommand[CONFIG_SYS_CBSIZE] = { 0, }; int len; int rc = 1; int flag; #endif ... #endif /* CONFIG_BOOTCOUNT_LIMIT */ s = getenv ("bootcmd"); // uboot/include/configs/versatile.h에 bootcmd가 정의되어 있으면 가져온다. // 현재 #define CONFIG_BOOTCOMMAND "bootm 0x210000 0x510000" 로 설정 debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>"); if (bootdelay >= 0 && s && !abortboot (bootdelay)) { // abortboot 함수 내부에서 3초 이내에 입력이 없으면 자동 부팅 실행 # ifdef CONFIG_AUTOBOOT_KEYED int prev = disable_ctrlc(1); /* disable Control C checking */ # endif # ifndef CONFIG_SYS_HUSH_PARSER run_command (s, 0); // run_command 호출 # else parse_string_outer(s, FLAG_PARSE_SEMICOLON | FLAG_EXIT_FROM_LOOP); # endif
C
복사
common/main.c
int run_command (const char *cmd, int flag) { cmd_tbl_t *cmdtp; char cmdbuf[CONFIG_SYS_CBSIZE]; /* working copy of cmd */ char *token; /* start of token in cmdbuf */ char *sep; /* end of token (separator) in cmdbuf */ char finaltoken[CONFIG_SYS_CBSIZE]; char *str = cmdbuf; char *argv[CONFIG_SYS_MAXARGS + 1]; /* NULL terminated */ int argc, inquotes; int repeatable = 1; int rc = 0; printf ("[RUN_COMMAND] cmd[%p]=\"\n", cmd); puts (cmd ? cmd : "NULL"); /* use puts - string may be loooong */ puts ("\"\n"); clear_ctrlc(); /* forget any previous Control C */ if (!cmd || !*cmd) { return -1; /* empty command */ } if (strlen(cmd) >= CONFIG_SYS_CBSIZE) { puts ("## Command too long!\n"); return -1; } strcpy (cmdbuf, cmd); /* Process separators and check for invalid * repeatable commands */ printf ("[PROCESS_SEPARATORS] %s\n", cmd); while (*str) { /* * Find separator, or string end * Allow simple escape of ';' by writing "\;" */ for (inquotes = 0, sep = str; *sep; sep++) { if ((*sep=='\'') && (*(sep-1) != '\\')) inquotes=!inquotes; if (!inquotes && (*sep == ';') && /* separator */ ( sep != str) && /* past string start */ (*(sep-1) != '\\')) /* and NOT escaped */ break; } /* * Limit the token to data between separators */ token = str; if (*sep) { str = sep + 1; /* start of command for next pass */ *sep = '\0'; } else str = sep; /* no more commands for next pass */ #ifdef DEBUG_PARSER printf ("token: \"%s\"\n", token); #endif /* find macros in this token and replace them */ process_macros (token, finaltoken); /* Extract arguments */ if ((argc = parse_line (finaltoken, argv)) == 0) { rc = -1; /* no command at all */ continue; } /* Look up command in command table */ if ((cmdtp = find_cmd(argv[0])) == NULL) { // cmd 인자 찾기. 현재 argv[0]는 bootm임 // bootm command와 일치하는 command list에서 찾아오기 printf ("Unknown command '%s' - try 'help'\n", argv[0]); rc = -1; /* give up after bad command */ continue; } printf("argv[0] : %s\n",argv[0]); // for debug -> 'bootm' /* found - check max args */ if (argc > cmdtp->maxargs) { cmd_usage(cmdtp); rc = -1; continue; } #if defined(CONFIG_CMD_BOOTD) /* avoid "bootd" recursion */ if (cmdtp->cmd == do_bootd) { printf ("[%s]\n", finaltoken); if (flag & CMD_FLAG_BOOTD) { puts ("'bootd' recursion detected\n"); rc = -1; continue; } else { flag |= CMD_FLAG_BOOTD; } } #endif /* OK - call function to do the command */ // cmd_bootm.c의 do_bootm() 함수 실행 if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) { rc = -1; } repeatable &= cmdtp->repeatable; /* Did the user stop this? */ if (had_ctrlc ()) return -1; /* if stopped then not repeatable */ } return rc ? rc : repeatable; }
C
복사
common/cmd_bootm.c
int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]) { ulong iflag; ulong load_end = 0; int ret; boot_os_fn *boot_fn; #ifndef CONFIG_RELOC_FIXUP_WORKS static int relocated = 0; /* relocate boot function table */ if (!relocated) { int i; for (i = 0; i < ARRAY_SIZE(boot_os); i++) if (boot_os[i] != NULL) boot_os[i] += gd->reloc_off; relocated = 1; } #endif /* determine if we have a sub command */ if (argc > 1) { char *endp; printf("argv[0], argv[1] -> %s, %s",argv[0],argv[1]); simple_strtoul(argv[1], &endp, 16); /* endp pointing to NULL means that argv[1] was just a * valid number, pass it along to the normal bootm processing * * If endp is ':' or '#' assume a FIT identifier so pass * along for normal processing. * * Right now we assume the first arg should never be '-' */ if ((*endp != 0) && (*endp != ':') && (*endp != '#')) { printf("start do_bootm_subcommand"); return do_bootm_subcommand(cmdtp, flag, argc, argv); } } printf("start bootm_start"); if (bootm_start(cmdtp, flag, argc, argv)) // 부팅 image 찾기, kernel+ramdisk return 1; /* * We have reached the point of no return: we are going to * overwrite all exception vector code, so we cannot easily * recover from any failures any more... */ iflag = disable_interrupts(); #if defined(CONFIG_CMD_USB) /* * turn off USB to prevent the host controller from writing to the * SDRAM while Linux is booting. This could happen (at least for OHCI * controller), because the HCCA (Host Controller Communication Area) * lies within the SDRAM and the host controller writes continously to * this area (as busmaster!). The HccaFrameNumber is for example * updated every 1 ms within the HCCA structure in SDRAM! For more * details see the OpenHCI specification. */ usb_stop(); #endif #ifdef CONFIG_AMIGAONEG3SE /* * We've possible left the caches enabled during * bios emulation, so turn them off again */ icache_disable(); dcache_disable(); #endif ret = bootm_load_os(images.os, &load_end, 1); // 부팅 image 로딩, kernel // bootm_load_os 함수는 아래에서 확인 ... boot_fn = boot_os[images.os.os]; if (boot_fn == NULL) { if (iflag) enable_interrupts(); printf ("ERROR: booting os '%s' (%d) is not supported\n", genimg_get_os_name(images.os.os), images.os.os); show_boot_progress (-8); return 1; } arch_preboot_os(); boot_fn(0, argc, argv, &images); //실제 kernel로 진입. -> do_bootm_linux 호출 ...
C
복사
bootm_load_os
lib_arm/bootm.c
int do_bootm_linux(int flag, int argc, char *argv[], bootm_headers_t *images) { bd_t *bd = gd->bd; char *s; int machid = bd->bi_arch_number; void (*theKernel)(int zero, int arch, uint params); #ifdef CONFIG_CMDLINE_TAG char *commandline = getenv ("bootargs"); //bootargs를 가져와서 commandline에 저장 printf("in do_bootm_linux-> commandline : %s\n",commandline); #endif if ((flag != 0) && (flag != BOOTM_STATE_OS_GO)) return 1; theKernel = (void (*)(int, int, uint))images->ep; s = getenv ("machid"); // machine id를 가져온다 if (s) { machid = simple_strtoul (s, NULL, 16); printf ("Using machid 0x%x from environment\n", machid); } show_boot_progress (15); debug ("## Transferring control to Linux (at address %08lx) ...\n", (ulong) theKernel); #if defined (CONFIG_SETUP_MEMORY_TAGS) || \ defined (CONFIG_CMDLINE_TAG) || \ defined (CONFIG_INITRD_TAG) || \ defined (CONFIG_SERIAL_TAG) || \ defined (CONFIG_REVISION_TAG) || \ defined (CONFIG_LCD) || \ defined (CONFIG_VFD) setup_start_tag (bd); #ifdef CONFIG_SERIAL_TAG setup_serial_tag (&params); #endif #ifdef CONFIG_REVISION_TAG setup_revision_tag (&params); #endif #ifdef CONFIG_SETUP_MEMORY_TAGS setup_memory_tags (bd); #endif #ifdef CONFIG_CMDLINE_TAG setup_commandline_tag (bd, commandline); #endif #ifdef CONFIG_INITRD_TAG if (images->rd_start && images->rd_end) setup_initrd_tag (bd, images->rd_start, images->rd_end); #endif #if defined (CONFIG_VFD) || defined (CONFIG_LCD) setup_videolfb_tag ((gd_t *) gd); #endif setup_end_tag (bd); #endif /* we assume that the kernel is in place */ printf ("\nStarting kernel ...\n\n"); #ifdef CONFIG_USB_DEVICE { extern void udc_disconnect (void); udc_disconnect (); } #endif cleanup_before_linux (); theKernel (0, machid, bd->bi_boot_params); // kernel image로 제어권을 넘기기 /* does not return */ return 1; }
C
복사
실제 u-boot를 빌드해서 커널이 메모리 어디에 올라가는지 확인해보자.
위 글의 실습 환경을 기준으로 분석하겠다.
현재 flash.bin 구조는 위 그림과 같다. ROM에 위치한 firmware인 flash.bin이 메모리에 올라가면
메모리 0번지에는 일종의 ROM코드가 들어가고 +0x10000 offset 부터 u-boot 코드가 들어간다.
0x210000에는 uimage가 들어간다.(rom code +0x10000이 있으므로 0x200000 + 0x10000)
uimage 헤더를 제외한 실제 커널 주소는 0x210040 이다.
bootcommand는 bootm 0x21000 0x510000으로 설정되어 있다
bootm [커널 주소] [ramdisk_주소] [dtb_주소]
따라서 0x210000 주소에 커널이 들어있으며 0x510000 주소에 filesystem이 들어있다.
uimage 헤더에는 현재 load 주소와 ep 주소가 0x500으로 설정되어 있다. load 주소와 image_start 주소(커널 헤더 제외 데이터 offset : 0x210040)가 다르기 때문에 bootm_load_os 함수 내부에서 아래 로직을 타게 된다.
load : 0x500
blob_start : 0x210000
image_start = 0x210040
따라서 image_start 주소 즉 실제 커널 데이터를 load에 넣는다. 이 말은 메모리의 0x500 주소에 커널을 올리고 그곳에서 커널 로딩을 한다는 뜻이다.
qemu의 uboot → kernel 부팅 로그를 확인해보면 설명한 것과 동일한 것을 알 수 있다.

5. 참고자료