[Linux Foundation](- https://youtu.be/INWghYZH3hI?si=8K44yBoVqtcxhiFB)
위 U-Boot 부트로더 설명 영상을 보며 정리해보고,
코드를 타고타고 따라가보았다.
코드는 원래 노션 토글로 간추렸던 것들을 여기에 일단 다 옮겨보았는데, 좀 긴 편이니 아랫 부분에 두었습니다.
이글 전에 첫번재 작성한 U-Boot글이 더 쉽게 정리해놓았긴합니다만
U-Boot 사, Linux Foundation의 영상을 바탕으로도 정리해보았습니다. 부족한 내용정리라, 많은 조언 부탁드립니다~~

1. Boot ROM
- 전원 인가를 하게 되면 SoC칩 내부에서 실행이 되는 코드(ROM). (EL3 익셉션 레벨3)
- 칩 내부의 H/W의 기본 유닛을 초기화 설정.
- Resides on reset vector(전원인가→리셋 벡터→boot ROM)
- BootROM: on-chip boot loader 라고도 함.
- Power-On Self-Test(POST) 자가 진단 수행 후, 비휘발성 메모리(NVRAM이나 CMOS)에 저장된 부팅 설정을 읽는다.
- 부팅 디바이스 탐색 및 선택 후 1차 Bootloader를 로드한다
- (Broadcom BCM2711)
2. 1st Bootloader
- DRAM 컨트롤러 초기화 전. (가상 메모리X, DRAMX).
- First bootloader는 램에서 실행되는게 아니라 칩 내부에 있는 Static RAM에서 실행되도록 보통 설계된다.
- PMU,GPIO,UART,PMIC(전원 칩),DDR 컨트롤러와 같은 여러 IP를 초기화하는 역할(전원이나 클럭같은 기본적인 설정)
- 2차 부트로더 로드(EL3→EL2)
- bootcode.bin
- U-Boot SPL(secondary program loader) (부트로더로 U-Boot를 쓸때)
- u-boot.bin은 바로 boot ROM이 로드 하기에 커서, SPL으로 대신 로드(SRAM).
- First user-controlled code(Boot ROM이후)
- Responsible for additional HW initialization(DRAM,UART,i2C, PMIC, MMC등 H/W 초기화)
- Loads U-Boot or Kernel directly
- https://docs.u-boot.org/en/latest/usage/spl_boot.html
3. 2nd Boot loader(User boot loader, U-boot)
- RAM에서 실행 (EL2)
- 전체 하드웨어 초기화 board_init_f(),crto.s,board_init_r()
- GPIO, UART, I2C, 타이머 등 주변 장치 초기화, 메모리 재배치,초기화
- SD카드로 부터 mmc를 이용해 디바이스 트리(device tree)와 커널 이미지를 메모리에 로드.
- Rootfs 경로를 bootargs로 커널에 전달.
- 하드웨어 구성 정보를 담고 있는 구조체의 포인터 전달.
- 커널에 커맨드 라인 포인터를 전달.
- 제어권을 커널에게 넘겨주고, 메모리를 반환후 종료.
4. Linux Kernel
- 커널 이미지 실행되면서 디바이스 트리 참조해서 초기화 수행(EL1)
- startup code (head.S)
- 주요 동작
- MMU/캐시 설정
- 디바이스 트리 파싱
- 메모리/디바이스 초기화
- mnt/root의 ext4 파일시스템을 “/”루트파일시스템로 mount 시도
- →부팅 중 메시지로 출력
5. root FS- User space Process(init)
- kernel_init()으로 PID1인 init 프로세스 실행
- 루트 파일시스템에서 /sbin/init, systemd, init.d 스크립트 등 실행
- system call, 쉘 등 사용자 공간 프로세스 시작
리셋부터, u-boot 부트로더 동작, init/main.c start_kernel()까지 소스코드
start.S ENTRY(_start)부터 start_kernel()까지 (cmd처리 중심)
ENTRY(_start)
파일 : arch/arm/cpu/armv8/start.S

bl lowlevel_init


board_init_f()까지 코드를 안전하게 실행하기 위한 최소한의 초기화.
가능한 아무것도 하지 않는다.
arch/arm/mach-bcm283x/lowlevel\_init.S
)3.start.s로 돌아와 bl _main으로 crto.s의 main으로 점프한다.

4.crt0.s:arch/arm/lib/crt0_64.s
- crt0\_64.s 설명 주석코드 

아래 보다시피


메모리 영역 재배치, bss영역 초기화 후 board_init_f로 분기한다.
crt0.s⇒다음 세가지 포함.
- board_init_f
- 하드웨어 초기사용을 위한 함수. 초기 메모리가 부족한 시점에서 실행되어 시스템 메모리(DRAM) 셋업 등 필수 환경을 준비.
- relocate_code
- U-Boot 바이너리를 실행 가능한 위치(DRAM 등)로 복사하고, 내부 주소를 재조정해 코드가 정상 실행되게 함.
- board_init_r
- 부팅 후반부 초기화 함수. 파일 시스템, 네트워크, 콘솔 등 다양한 장치를 초기화하며, 사용자 명령 입력 대기 상태로 전환.
1.board_init_f()
u-boot/common/board_f.c

board_init_r()로 넘어가기 전까지 시스템 메모리 초기화(SDRAM) 및 UART 등 기본 I/O준비.

다시 crto_64.s의 main으로 돌아간 후
메모리 재배치, bss영역 초기화 등 다음, board_init_r 호출

2.board_init_r()
u-boot/common/board_r.c



board_init f, r()로 2차 부트로더의 하드웨어, 주변장치 초기화 단계를 수행 후,
main_loop()→main.c로 진입한다.
3.common/main.c로 진입



사용자 버튼 처리(예 : GPIO 버튼 누를때 동작)
bootdelay처리,
부팅할 명령어 실행
cli.c→ cli_loop()→cli_simple_loop호출
common/cli_simple.c
)
입력받기 위해 cli_readline 호출
common/cli_readline.c
cli_readline_into_buffer
cread_line_simple

- cread_line_simple 전체코드
static int cread_line_simple(const char *const prompt, char *p)
{
char *p_buf = p;
int n = 0; /* buffer index */
int plen = 0; /* prompt length */
int col; /* output column cnt */
int c;
/* print prompt */
if (prompt) {
plen = strlen(prompt);
puts(prompt);
}
col = plen;
for (;;) {
if (bootretry_tstc_timeout())
return -2; /* timed out */
schedule(); /* Trigger watchdog, if needed */
c = getchar();
/*
* Special character handling
*/
switch (c) {
case '\r': /* Enter */
case '\n':
*p = '\0';
puts("\r\n");
return p - p_buf;
case '\0': /* nul */
continue;
case 0x03: /* ^C - break */
p_buf[0] = '\0'; /* discard input */
return -1;
case 0x15: /* ^U - erase line */
while (col > plen) {
puts(erase_seq);
--col;
}
p = p_buf;
n = 0;
continue;
case 0x17: /* ^W - erase word */
p = delete_char(p_buf, p, &col, &n, plen);
while ((n > 0) && (*p != ' '))
p = delete_char(p_buf, p, &col, &n, plen);
continue;
case 0x08: /* ^H - backspace */
case 0x7F: /* DEL - backspace */
p = delete_char(p_buf, p, &col, &n, plen);
continue;
default:
/* Must be a normal character then */
if (n >= CONFIG_SYS_CBSIZE - 2) { /* Buffer full */
putc('\a');
break;
}
if (c == '\t') { /* expand TABs */
if (IS_ENABLED(CONFIG_AUTO_COMPLETE)) {
/*
* if auto-completion triggered just
* continue
*/
*p = '\0';
if (cmd_auto_complete(prompt,
console_buffer,
&n, &col)) {
p = p_buf + n; /* reset */
continue;
}
}
puts(tab_seq + (col & 07));
col += 8 - (col & 07);
} else {
char __maybe_unused buf[2];
/*
* Echo input using puts() to force an LCD
* flush if we are using an LCD
*/
++col;
buf[0] = c;
buf[1] = '\0';
puts(buf);
}
*p++ = c;
++n;
break;
}
}
}사용자가 U-Boot 프롬프트에 입력한 문자열은 p_buf에 저장됨.
엔터 치면 cread_line_simple은 입력한 문자열 길이를 리턴하고,
그 다음 이 문자열은 명령어 파싱기로 넘어감.
simple_loop로 돌아와 문자열 길이를 받으면 last_command에 문자열을 복사해 rum_command를 실행

run_command_list (cli.c)
int run_command_list(const char *cmd, int len, int flag)
{
int need_buff = 1;
char *buff = (char *)cmd; /* cast away const */
int rcode = 0;
if (len == -1) {
len = strlen(cmd);
#ifdef CONFIG_HUSH_PARSER
/* hush will never change our string */
need_buff = 0;
#else
/* the built-in parser will change our string if it sees \n */
need_buff = strchr(cmd, '\n') != NULL;
#endif
}
if (need_buff) {
buff = malloc(len + 1);
if (!buff)
return 1;
memcpy(buff, cmd, len);
buff[len] = '\0';
}
#ifdef CONFIG_HUSH_PARSER
if (use_hush_old()) {
rcode = parse_string_outer(buff, FLAG_PARSE_SEMICOLON);
} else {
rcode = parse_string_outer_modern(buff, FLAG_PARSE_SEMICOLON);
}
#else
/*
* This function will overwrite any \n it sees with a \0, which
* is why it can't work with a const char *. Here we are making
* using of internal knowledge of this function, to avoid always
* doing a malloc() which is actually required only in a case that
* is pretty rare.
*/
#ifdef CONFIG_CMDLINE
rcode = cli_simple_run_command_list(buff, flag);
#else
rcode = board_run_command(buff);
#endif
#endif
if (need_buff)
free(buff);
return rcode;
}- cmd_process (command.c)
enum command_ret_t cmd_process(int flag, int argc, char *const argv[],
int *repeatable, ulong *ticks)
{
enum command_ret_t rc = CMD_RET_SUCCESS;
struct cmd_tbl *cmdtp;
#if defined(CONFIG_SYS_XTRACE)
char *xtrace;
xtrace = env_get("xtrace");
if (xtrace) {
puts("+");
for (int i = 0; i < argc; i++) {
puts(" ");
puts(argv[i]);
}
puts("\n");
}
#endif
/* Look up command in command table */
cmdtp = find_cmd(argv[0]);
if (cmdtp == NULL) {
printf("Unknown command '%s' - try 'help'\n", argv[0]);
return 1;
}
/* found - check max args */
if (argc > cmdtp->maxargs)
rc = CMD_RET_USAGE;
#if defined(CONFIG_CMD_BOOTD)
/* avoid "bootd" recursion */
else if (cmdtp->cmd == do_bootd) {
if (flag & CMD_FLAG_BOOTD) {
puts("'bootd' recursion detected\n");
rc = CMD_RET_FAILURE;
} else {
flag |= CMD_FLAG_BOOTD;
}
}
#endif
/* If OK so far, then do the command */
if (!rc) {
int newrep;
if (ticks)
*ticks = get_timer(0);
rc = cmd_call(cmdtp, flag, argc, argv, &newrep);
if (ticks)
*ticks = get_timer(*ticks);
*repeatable &= newrep;
}
if (rc == CMD_RET_USAGE)
rc = cmd_usage(cmdtp);
return rc;
}/* Look up command in command table */
cmdtp = find_cmd(argv[0]);
find_cmd()(command.c)
find_cmd(”boot”)는 cmdtp→cmd_rep(아래 설명)== do_booti_wrapper인 구조체를 찾아 반환한다.

cmd_call(command.c)

cmd_tbl을 통해 cmd_rep()으로 호출 (command.h)

- cmd_rep는 struct cmd_tbl 내 명령어 실행 함수 포인터
- 명령어 등록 매크로(U_BOOT_CMD 등)가 이 함수 포인터를 연결
command.h

- cmd/booti.c

이 매크로는 내부적으로 __u_boot_cmd라는 특수 섹션에 struct cmd_tbl을 등록, 이 구조체의 .cmd_rep 필드는 do_booti(), do_booti_wrapper()포인터가 된다.
- do_booti함수 실행 (cmd/booti.c)
int do_booti(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
struct bootm_info bmi;
int states;
int ret;
/* Consume 'booti' */
argc--; argv++;
bootm_init(&bmi);
if (argc)
bmi.addr_img = argv[0];
if (argc > 1)
bmi.conf_ramdisk = argv[1];
if (argc > 2)
bmi.conf_fdt = argv[2];
bmi.boot_progress = true;
bmi.cmd_name = "booti";
/* do not set up argc and argv[] since nothing uses them */
if (booti_start(&bmi))
return 1;
/*
* We are doing the BOOTM_STATE_LOADOS state ourselves, so must
* disable interrupts ourselves
*/
bootm_disable_interrupts();
images.os.os = IH_OS_LINUX;
if (IS_ENABLED(CONFIG_RISCV))
if (IS_ENABLED(CONFIG_64BIT))
images.os.arch = IH_ARCH_RISCV64;
else
images.os.arch = IH_ARCH_RISCV;
else if (IS_ENABLED(CONFIG_ARM64))
images.os.arch = IH_ARCH_ARM64;
states = BOOTM_STATE_MEASURE | BOOTM_STATE_OS_PREP |
BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO;
if (IS_ENABLED(CONFIG_SYS_BOOT_RAMDISK_HIGH))
states |= BOOTM_STATE_RAMDISK;
ret = bootm_run_states(&bmi, states);
return ret;
}- 커널 entry로 점프
- boot_jump_linux (bootm.c) 커널 entry주소로 점프 (보통 Image or vmlinux 시작 주소)
int do_bootm_linux(int flag, struct bootm_info *bmi)
{
struct bootm_headers *images = bmi->images;
/* No need for those on ARM */
if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
return -1;
if (flag & BOOTM_STATE_OS_PREP) {
boot_prep_linux(images);
return 0;
}
if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
boot_jump_linux(images, flag);
return 0;
}
boot_prep_linux(images);
boot_jump_linux(images, flag);
return 0;
}
#if defined(CONFIG_BOOTM_VXWORKS)
void boot_prep_vxworks(struct bootm_headers *images)
{
#if defined(CONFIG_OF_LIBFDT)
int off;
if (images->ft_addr) {
off = fdt_path_offset(images->ft_addr, "/memory");
if (off > 0) {
if (arch_fixup_fdt(images->ft_addr))
puts("## WARNING: fixup memory failed!\n");
}
}
#endif
cleanup_before_linux();
}
void boot_jump_vxworks(struct bootm_headers *images)
{
#if defined(CONFIG_ARM64) && defined(CONFIG_ARMV8_PSCI)
armv8_setup_psci();
smp_kick_all_cpus();
#endif
/* ARM VxWorks requires device tree physical address to be passed */
((void (*)(void *))images->ep)(images->ft_addr);
}
#endif /* Subcommand: GO */
static void boot_jump_linux(struct bootm_headers *images, int flag)
{
#ifdef CONFIG_ARM64
void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
void *res2);
int fake = (flag & BOOTM_STATE_OS_FAKE_GO);
kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
void *res2))images->ep;
debug("## Transferring control to Linux (at address %lx)...\n",
(ulong) kernel_entry);
bootstage_mark(BOOTSTAGE_ID_RUN_OS);
announce_and_cleanup(fake);
if (!fake) {
do_nonsec_virt_switch();
update_os_arch_secondary_cores(images->os.arch);
#else
if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
(images->os.arch == IH_ARCH_ARM))
armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
(u64)images->ft_addr, 0,
(u64)images->ep,
ES_TO_AARCH32);
else
armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
images->ep,
ES_TO_AARCH64);
#endif
}
#else
#endif
}arch/arm/cpu/armv8/transition.S
.pushsection .text.armv8_switch_to_el2, "ax"
ENTRY(armv8_switch_to_el2)
bl armv8_switch_to_el2_prep
nop
switch_el x6, 1f, 0f, 0f
0:
cmp x5, #ES_TO_AARCH64
b.eq 2f
/*
* When loading 32-bit kernel, it will jump
* to secure firmware again, and never return.
*/
bl armv8_el2_to_aarch32
2:
/*
* x4 is kernel entry point or switch_to_el1
* if CONFIG_ARMV8_SWITCH_TO_EL1 is defined.
* When running in EL2 now, jump to the
* address saved in x4.
*/
br x4
1: armv8_switch_to_el2_m x4, x5, x6
ENDPROC(armv8_switch_to_el2)
.popsectionstarting kernel 메세지 출력 (common/bootm.c) 메세지는 u-boot에서 출력된다.
(하드웨어관련 메세지도 추가하기)

커널 이미지 로딩 후 , head.S 실행. ~/linux/arch/arm64/kernerl/head.S에서 start_kernel()로 진입

init/main.c :start_kernel() 진입해 초기화
[U-Boot]
cli_loop()
└─> cli_simple_loop()
└─> cli_readline() // 콘솔에서 사용자 명령어 입력 대기 및 읽기
└─> run_command("booti 80000 - 90000") // 입력받은 명령어 실행 요청
└─> run_command_list() // 명령어가 여러 개일 경우 분리해서 처리
└─> cmd_process() // 명령어 문자열에서 명령어 구조체 검색 및 실행
└─> find_cmd("booti") // 명령어 테이블에서 'booti' 명령어 찾음
└─> do_booti() // 'booti' 명령어의 실제 실행 함수 호출 (cmd/booti.c)
→ booti_start(&bmi)
→ bootm_disable_interrupts()
→ bootm_run_states()
└─> BOOTM_STATE_OS_PREP
└─> BOOTM_STATE_OS_GO
└─> arch/arm/lib/bootm.c: do_bootm_linux()
└─> kernel 이미지 주소로 분기 → 커널 실행!
[Linux Kernel]
arch/arm64/kernel/head.S
└─> stext
└─> start_kernel()
└─> rest_init()
└─> kernel_init()
⇒ 전원을 켰을 때 main loop 안의 autoboot에 의해 시간 내에
키보드 입력이 들어오면 u-boot 프롬프트로 진입하고, run bootcmd(디폴트 부팅 커맨드)를 입력하면 부팅하여 start kernel 한다.
이제 명령어 해석기 (CLI)에서 계속 커맨드 받는 동작!!
start_kernel()이후 첫 프로세스 실행까지
- start_kernel()이후 소스코드 동작 (보완하기)
// Kernel startup entry point
- 부트로더가 ARM64의 레지스터 x0에 디바이스 트리의 주소를 넣어주고,
커널이 하드웨어 정보를 읽는다.
(MMU랑 데이터 캐시 등이 비활성화된 상태여야 함)
x0 = __pa(KERNEL_START)
bl start_kernel
init/main.c 의 start_kernel()
- 여기서 시스템 전역 데이터 구조 초기화, 스케줄러, 인터럽트, 타이머, 콘솔 등 핵심 서브시스템이 순차적으로 초기화된다.

start_kernel()의 마지막 rest_init()이 호출되고, 하는 역할
첫번째 커널 스레드 생성 kernel_init()→ do_initcalls()를 통해 각종 서브시스템, 드라이버, 모듈을 초기화하는 스레드이다.

 static noinline void __ref __noreturn rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
// 유저모드 스레드 생성1
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
// 커널모드 스레드 생성2
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
system_state = SYSTEM_SCHEDULING;
/* Call into cpu_idle with preempt disabled */
cpu_startup_entry(CPUHP_ONLINE);
}----------------------------------------------------------------- void cpu_startup_entry(enum cpuhp_state state)
{
current->flags |= PF_IDLE;
arch_cpu_idle_prepare();
cpuhp_online_idle(state);
while (1)
do_idle();
}- kernel_init()내부 주요 흐름
- 루트 파일 시스템 마운트 시도 - initrd나 initramfs처리 - do_basic_setup()→do_initcalls()호출
static int __ref kernel_init(void *unused)
{
kernel_init_freeable();
-> do_basic_setup(); -> do_initcalls(); 호출
=> 유저 모드를 사용하기 전,
커널의 주요 기능을 사용하기 위한 초기화 완료- do_initcalls()는
- 커널에 등록된 각종 초기화 함수들을 실행 (드라이버, 서브시스템 등) - __initcall_start ~ __initcall_end 섹션의 함수들을 순차적으로 호출 - 두번째 스레드 : do_idle()로 진입해 스케줄러가 일을 줄 때까지 대기
사용자 공간(userspace) 진입
- kernel_init() 함수가 마지막으로 init 프로세스(PID 1)를 실행.
- /sbin/init, 또는 systemd, upstart, busybox init 등이 사용된다.
- 이후 사용자 공간에서 서비스, 로그인, 데스크탑 환경 등 다양한 프로세스가 실행됨.
static int run_init_process(const char *init_filename)
{
const char *const *p;
argv_init[0] = init_filename;
pr_info("Run %s as init process\n", init_filename);
pr_debug(" with arguments:\n");
for (p = argv_init; *p; p++)
pr_debug(" %s\n", *p);
pr_debug(" with environment:\n");
for (p = envp_init; *p; p++)
pr_debug(" %s\n", *p);
return kernel_execve(init_filename, argv_init, envp_init);
}'Linux' 카테고리의 다른 글
| [리눅스 커널과 디바이스 드라이버]애플리케이션->syscall->vfs->디바이스파일open 까지 (2) | 2025.08.17 |
|---|---|
| 리눅스 디바이스 드라이버 - module_init() 모듈 등록부터 사용자 호출까지 (1) | 2025.08.17 |
| syscall번호를 테이블에 넣어 커널에 빌트인하여 사용 (2) | 2025.08.17 |
| UART3 활성화하여 PC와 시리얼 통신[dtoverlay 사용해보기] (1) | 2025.08.16 |
| U-Boot 빌드 과정 흐름 요약 (1) | 2025.08.16 |