QUESTION
init_primary()가 실제로 초기화하는 범위는?
OP-TEE OS 4.10.0의 init_primary()는 primary 부트 C 경로의 한 구간이다. 이 함수는 예외 mask, heap pool, 저장된 memory map, 선택적 pager, stack canary와 CPU별 vector를 준비한다. thread pool과 서비스 initcall 전체를 혼자 실행하지 않는다.
boot_init_primary_late(), boot_init_primary_runtime(), boot_init_primary_final()의 설정별 후속 동작을 분리해 읽는다.
STRUCTURE
초기 C 문맥과 runtime 객체의 경계
pager의 임시 curr_thread=0과 뒤의 일반 thread pool은 다른 사건이다.
CALL PATH
pager와 NS 가상화의 조건부 흐름
위쪽은 init_primary 내부의 pager 분기이고 아래쪽은 이후 단계의 NS 가상화 분기다. 두 설정은 별도로 읽는다. init_tee_runtime()은 비가상화 runtime 분기에서 실행한다. call_finalcalls()는 공통 final 단계다.
STATE LEDGER
함수별로 남는 상태
| 함수·범위 | 생성·변경 | 조건과 다음 소비자 |
|---|---|---|
init_primary L894–955 | 예외 mask, heap pool, saved core map, phys memory, boot allocator 정리 | pager·가상화·동적 설정에 따라 pool과 allocator가 다름 |
init_primary L957–972 | 선택적 pager, stack canary, per-CPU vector | CFG_WITH_PAGER이면 curr_thread=0 지정 후 pager |
primary_late L1017–1041 | DT·NS memory, core-local; 비가상화면 pool과 boot thread | 가상화면 curr_thread=-1로 비움 |
primary_runtime L1043–1090 | user kcode, intc, native IRQ 정책, TEE initcalls | init_tee_runtime()과 IRQ unmask는 비가상화만 |
primary_final L1092–1105 | 비가상화 driver initcall, 공통 finalcalls | Normal World 전환 전 native IRQ mask |
initcall.c L18–25는 실패를 로그에 남기고 다음 callback으로 진행한다. 성공이나 역순 rollback을 일괄 가정하지 않는다.
VISUAL WALKTHROUGH
부트 단계별 역할
부트 흐름과 각 단계에서 준비하는 상태를 함수별로 구분한다.
heap/pager는 init_primary, DT/thread array는 후속 late 단계다.
앞쪽 상태를 뒤쪽에서 처음 사용하는 관계다.
boot map→core_mmu_save_mem_map→runtime mapheap→pager (선택)→pageable executionprimary_late→thread pool (비가상화)→SMC slotDT / NS memory→TEE initcalls→service pathUPSTREAM SOURCE
원본 코드
아래 코드는 OP-TEE OS 4.10.0의 core/arch/arm/kernel/boot.c에서 859-947줄을 그대로 가져온 것이다. 설명을 위해 실제 코드를 가짜 의사 코드로 바꾸지 않았다.
859 thread_init_core_local_pauth_keys();
860 thread_init_thread_pauth_keys();
861
862 /*
863 * Reinitialize canaries around the stacks with crypto_rng_read().
864 *
865 * TODO: Updating canaries when CFG_NS_VIRTUALIZATION is enabled will
866 * require synchronization between thread_check_canaries() and
867 * thread_update_canaries().
868 */
869 if (!IS_ENABLED(CFG_NS_VIRTUALIZATION))
870 thread_update_canaries();
871}
872
873static bool add_padding_to_pool(vaddr_t va, size_t len, void *ptr __unused)
874{
875#ifdef CFG_NS_VIRTUALIZATION
876 nex_malloc_add_pool((void *)va, len);
877#else
878 malloc_add_pool((void *)va, len);
879#endif
880 return true;
881}
882
883static void init_primary(unsigned long pageable_part)
884{
885 vaddr_t va = 0;
886
887 /*
888 * Mask asynchronous exceptions before switch to the thread vector
889 * as the thread handler requires those to be masked while
890 * executing with the temporary stack. The thread subsystem also
891 * asserts that the foreign interrupts are blocked when using most of
892 * its functions.
893 */
894 thread_set_exceptions(THREAD_EXCP_ALL);
895 primary_save_cntfrq();
896 init_vfp_sec();
897
898 if (IS_ENABLED(CFG_CRYPTO_WITH_CE))
899 check_crypto_extensions();
900
901 init_asan();
902
903 /*
904 * By default whole OP-TEE uses malloc, so we need to initialize
905 * it early. But, when virtualization is enabled, malloc is used
906 * only by TEE runtime, so malloc should be initialized later, for
907 * every virtual partition separately. Core code uses nex_malloc
908 * instead.
909 */
910#ifdef CFG_WITH_PAGER
911 /* Add heap2 first as heap1 may be too small as initial bget pool */
912 malloc_add_pool(__heap2_start, __heap2_end - __heap2_start);
913#endif
914#ifdef CFG_NS_VIRTUALIZATION
915 nex_malloc_add_pool(__nex_heap_start, __nex_heap_end -
916 __nex_heap_start);
917#else
918 malloc_add_pool(__heap1_start, __heap1_end - __heap1_start);
919#endif
920 IMSG_RAW("\n");
921 if (IS_ENABLED(CFG_DYN_CONFIG)) {
922 size_t sz = sizeof(struct thread_core_local) *
923 CFG_TEE_CORE_NB_CORE;
924 void *p = boot_mem_alloc(sz, alignof(void *) * 2);
925
926#ifdef CFG_NS_VIRTUALIZATION
927 nex_malloc_add_pool(p, sz);
928#else
929 malloc_add_pool(p, sz);
930#endif
931 }
932
933 core_mmu_save_mem_map();
934 core_mmu_init_phys_mem();
935 boot_mem_foreach_padding(add_padding_to_pool, NULL);
936 va = boot_mem_release_unused();
937 if (!IS_ENABLED(CFG_WITH_PAGER)) {
938 /*
939 * We must update boot_cached_mem_end to reflect the memory
940 * just unmapped by boot_mem_release_unused().
941 */
942 assert(va && va <= boot_cached_mem_end);
943 boot_cached_mem_end = va;
944 }
945
946 if (IS_ENABLED(CFG_DYN_CONFIG)) {
947 /*
READING CONTRACT
발췌 범위와 후속 함수
대표 코드 boot.c L859–947의 기준 정의는 L883이다. 발췌는 L947에서 끝나지만 함수는 L972까지 이어진다. 아래 관련 원문과 고정 commit 전체 파일을 함께 본다.
thread_init_primary()는 thread pool 생성 함수가 아니다. 비가상화 pool/boot-thread 설정은 primary_late L1037–1038이다.
RELATED SOURCE
함께 읽어야 하는 원본 코드
첫 코드 조각만으로 동작이 완성되지 않는 경우 호출 매크로, 객체 정의와 실제 실행 목록을 같은 페이지에 묶었다. 각 조각은 같은 기준 commit에서 가져왔다.
01. thread, service와 runtime initcall을 마무리하는 단계
core/arch/arm/kernel/boot.c 976-1080줄이다.
976#if defined(ARM32)
977 return read_sctlr() & SCTLR_NMFI;
978#else
979 /* Note: ARM64 does not feature non-maskable FIQ support. */
980 return false;
981#endif
982}
983
984/*
985 * Note: this function is weak just to make it possible to exclude it from
986 * the unpaged area.
987 */
988void __weak boot_init_primary_late(unsigned long fdt __unused,
989 unsigned long manifest __unused)
990{
991 size_t fdt_size = CFG_DTB_MAX_SIZE;
992
993 if (IS_ENABLED(CFG_TRANSFER_LIST) && mapped_tl) {
994 struct transfer_list_entry *tl_e = NULL;
995
996 tl_e = transfer_list_find(mapped_tl, TL_TAG_FDT);
997 if (tl_e) {
998 /*
999 * Expand the data size of the DTB entry to the maximum
1000 * allocable mapped memory to reserve sufficient space
1001 * for inserting new nodes, avoid potentially corrupting
1002 * next entries.
1003 */
1004 uint32_t dtb_max_sz = mapped_tl->max_size -
1005 mapped_tl->size + tl_e->data_size;
1006
1007 if (!transfer_list_set_data_size(mapped_tl, tl_e,
1008 dtb_max_sz)) {
1009 EMSG("Failed to extend DTB size to %#"PRIx32,
1010 dtb_max_sz);
1011 panic();
1012 }
1013 fdt_size = tl_e->data_size;
1014 }
1015 }
1016
1017 init_external_dt(boot_arg_fdt, fdt_size);
1018 reinit_manifest_dt();
1019#ifdef CFG_CORE_FFA
1020 tpm_map_log_area(get_manifest_dt());
1021#else
1022 tpm_map_log_area(get_external_dt());
1023#endif
1024 discover_nsec_memory();
1025 update_external_dt();
1026 configure_console_from_dt();
1027
1028 if (IS_ENABLED(CFG_NS_VIRTUALIZATION)) {
1029 /*
1030 * Virtualization: We can't initialize threads right now because
1031 * threads belong to "tee" part and will be initialized
1032 * separately per each new virtual guest. So, we'll clear
1033 * "curr_thread" and call it done.
1034 */
1035 thread_get_core_local()->curr_thread = -1;
1036 } else {
1037 thread_init_threads(CFG_NUM_THREADS);
1038 thread_init_boot_thread();
1039 }
1040 thread_init_thread_core_local(CFG_TEE_CORE_NB_CORE);
1041}
1042
1043void __weak boot_init_primary_runtime(void)
1044{
1045 thread_init_primary();
1046 IMSG("OP-TEE version: %s", core_v_str);
1047 if (IS_ENABLED(CFG_INSECURE)) {
1048 IMSG("WARNING: This OP-TEE configuration might be insecure!");
1049 IMSG("WARNING: Please check https://optee.readthedocs.io/en/latest/architecture/porting_guidelines.html");
1050 }
1051 IMSG("Primary CPU initializing");
1052#ifdef CFG_CORE_ASLR
1053 DMSG("Executing at offset %#lx with virtual load address %#"PRIxVA,
1054 (unsigned long)boot_mmu_config.map_offset, VCORE_START_VA);
1055#endif
1056#ifdef CFG_NS_VIRTUALIZATION
1057 DMSG("NS-virtualization enabled, supporting %u guests",
1058 CFG_VIRT_GUEST_COUNT);
1059#endif
1060 if (IS_ENABLED(CFG_MEMTAG))
1061 DMSG("Memory tagging %s",
1062 memtag_is_enabled() ? "enabled" : "disabled");
1063
1064 /* Check if platform needs NMFI workaround */
1065 if (cpu_nmfi_enabled()) {
1066 if (!IS_ENABLED(CFG_CORE_WORKAROUND_ARM_NMFI))
1067 IMSG("WARNING: This ARM core has NMFI enabled, please apply workaround!");
1068 } else {
1069 if (IS_ENABLED(CFG_CORE_WORKAROUND_ARM_NMFI))
1070 IMSG("WARNING: This ARM core does not have NMFI enabled, no need for workaround");
1071 }
1072
1073 boot_primary_init_intc();
1074 init_vfp_nsec();
1075 if (!IS_ENABLED(CFG_NS_VIRTUALIZATION)) {
1076 /*
1077 * Unmask native interrupts during driver initcalls.
1078 *
1079 * NS-virtualization still uses the temporary stack also
1080 * used for exception handling so it must still have native
02. linker section의 service initcall을 실행
core/kernel/initcall.c 43-74줄이다.
43 */
44void __weak call_early_initcalls(void)
45{
46 DO_INIT_CALLS(early_initcall);
47}
48
49/*
50 * Note: this function is weak just to make it possible to exclude it from
51 * the unpaged area.
52 */
53void __weak call_service_initcalls(void)
54{
55 DO_INIT_CALLS(service_initcall);
56}
57
58/*
59 * Note: this function is weak just to make it possible to exclude it from
60 * the unpaged area.
61 */
62void __weak call_driver_initcalls(void)
63{
64 DO_INIT_CALLS(driver_initcall);
65}
66
67/*
68 * Note: this function is weak just to make it possible to exclude it from
69 * the unpaged area.
70 */
71void __weak call_finalcalls(void)
72{
73 DO_INIT_CALLS(finalcall);
74}
DETAILS
초기화 경계의 차이
heap과 boot allocator
L910–955의 pool 선택과 boot allocator 정리는 pager·가상화·동적 설정에 따라 달라진다. 동일한 heap 경로를 모든 빌드에 적용할 수 없다.
임시 current thread
pager 빌드는 L965에서 curr_thread=0을 지정해 pager runtime을 초기화한다. 일반 thread array와 boot thread는 late 단계에서 별도로 설정한다.
initcall 반환
do_init_calls()는 오류를 로그로 남기지만 루프를 중단하거나 이전 callback을 역순 정리하지 않는다.
IMPLEMENTATION NOTES
설정이 바꾸는 경로
CFG_WITH_PAGER
pageable_part는 pager 설정에서만 init_pager_runtime()에 전달된다. pager가 없으면 boot memory 해제 뒤 boot_cached_mem_end를 갱신한다.
CFG_NS_VIRTUALIZATION
late 단계에서 현재 thread를 비우고, runtime/final에서 일반 TEE·driver initcall을 건너뛴다. guest별 준비를 early boot 완료로 오해하지 않는다.
native interrupt mask
초기에는 예외를 모두 막고, 비가상화 runtime에서는 driver initcall 중 native interrupt를 풀었다가 final에서 다시 막는다.
DEEP ANALYSIS
첫 standard SMC 전에 확인할 계약
MMU·allocator 입력
설정별 pool
late 단계
비가상화 경로
요청별 검증
모든 칸이 모든 빌드에서 실행된다는 뜻은 아니다. message buffer 검사는 실제 SMC 요청 경로를 추적한다.
| 관찰 | 고정 소스 | 해석 |
|---|---|---|
| pager boot 중단 | boot.c L957–967 | pageable_part와 pool, 임시 current-thread 설정을 비교 |
| DT 빌드에서만 실패 | L1017–1026 | DT 초기화와 NS memory 발견을 분리 |
| 서비스 첫 요청 실패 | init_tee_runtime와 SMC path | initcall 로그와 요청별 validation 결과를 분리 |
| 가상화에서 pool 없음 | L1028–1039 | early boot의 의도적 지연인지 guest 생성 오류인지 구분 |
OBJECT LIFETIME
초기 객체의 수명
| 대상 | 생산·변경 | 확인 |
|---|---|---|
| boot memory map | core_mmu_save_mem_map() | runtime map에 필요한 항목 |
| malloc / nex_malloc pool | init_primary L910–931 | pager·가상화 설정별 pool |
| per-CPU vector | thread_init_per_cpu() | CPU별 exception 처리 |
| thread array / boot thread | primary_late 비가상화 분기 | runtime SMC에서 재사용 |
| initcall descriptor | linker section | dispatcher에 일반 rollback 없음 |
FAILURE PATH
실패 지점과 관찰
| 현상 | 먼저 볼 코드 | 판정 |
|---|---|---|
| pager 부팅 중단 | init_pager_runtime() | pageable_part·pool·설정 확인 |
| DT/NS memory 오류 | primary_late L1017–1025 | 해당 함수의 panic/진행 정책 개별 확인 |
| initcall 오류 로그 뒤 진행 | initcall.c L18–25 | 다음 callback과 첫 소비자도 확인 |
| 첫 SMC 실패 | thread allocation / message parser | pool 준비와 요청 검증을 분리 |
EVIDENCE
빌드 산출물과 로그
같은 빌드의 tee.elf를 읽는다. nm은 C 선언이 아니라 심볼 이름을 출력한다.
| 목적 | 읽기 전용 명령·관찰 | 해석 |
|---|---|---|
| section | ${CROSS_COMPILE64}readelf -W -lS out/arm/core/tee.elf | pager/pageable과 core 영역 배치 |
| 심볼 | ${CROSS_COMPILE64}nm -an out/arm/core/tee.elf | grep -E " (init_primary|boot_init_primary_late|boot_init_primary_runtime)$" | 함수 이름·링크 주소를 확인 |
| secure console | initcall 오류와 Normal World 전환 로그 | 오류 로그 뒤에도 다음 callback이 실행될 수 있음 |
| Linux 측 | dmesg | grep -i optee | 서비스별 성공과 별개로 해석 |
LAB
최소 계측 지점
- 01pager 조건
pageable_part, heap pool, pager 진입을 같은 설정에서 기록한다. - 02thread pool
primary_late의 비가상화 분기와 첫 SMC의 FREE slot 수를 비교한다. - 03initcall 오류
callback 이름·반환값과 첫 소비자의 결과를 기록한다. 자동 rollback을 가정하지 않는다.
PRIMARY REFERENCES