[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)

  1. bootcode.bin
  2. 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처리 중심)

  1. ENTRY(_start)
    파일 : arch/arm/cpu/armv8/start.S

  2. bl lowlevel_init


board_init_f()까지 코드를 안전하게 실행하기 위한 최소한의 초기화.
가능한 아무것도 하지 않는다.

  arch/arm/mach-bcm283x/lowlevel\_init.S  
![](https://velog.velcdn.com/images/yez_hong/post/42aff80b-2a3c-4984-86f1-192aefa5cdca/image.png))![](https://velog.velcdn.com/images/yez%5C_hong/post/42aff80b-2a3c-4984-86f1-192aefa5cdca/image.png)

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처리,

부팅할 명령어 실행

  1. 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

  1. 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;
            }
  1. 커널 entry로 점프
  2. - 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)
            .popsection

starting 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()를 통해 각종 서브시스템, 드라이버, 모듈을 초기화하는 스레드이다.

![](https://velog.velcdn.com/images/yez_hong/post/2fd2bf70-d4a2-4f30-93ae-ed3cdad6a9f2/image.png)
        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);
}

+ Recent posts