운영체제 기본/Linux의 ELF 로딩/초기 스택과 사용자 모드 진입: 두 판 사이의 차이
BIOS GRUB 부팅 경로 보강과 각론 형식 통일 |
첫 init의 실행과 ret_from_fork 설명을 부팅 장으로 이동 |
||
| (같은 사용자의 중간 판 하나는 보이지 않습니다) | |||
| 9번째 줄: | 9번째 줄: | ||
<code>execve(path, argv, envp)</code>에서 <code>argv</code>는 인자 문자열들의 배열이고, <code>envp</code>는 환경 변수 문자열들의 배열이다. 예를 들어 실행할 프로그램이 두 개의 인자를 받는다면, 새 프로그램은 인자 개수와 각 문자열의 주소를 알아야 한다. | <code>execve(path, argv, envp)</code>에서 <code>argv</code>는 인자 문자열들의 배열이고, <code>envp</code>는 환경 변수 문자열들의 배열이다. 예를 들어 실행할 프로그램이 두 개의 인자를 받는다면, 새 프로그램은 인자 개수와 각 문자열의 주소를 알아야 한다. | ||
호출 당시의 포인터는 이전 프로그램의 주소 공간을 가리킨다. exec는 그 주소 공간을 새 이미지로 교체하므로 포인터 값만 그대로 넘길 수 없다. 커널은 exec 준비 과정에서 문자열을 새 실행을 위한 메모리에 복사하고, 새 사용자 스택의 위치를 정한다. 이어 ELF 로더의 <code>create_elf_tables()</code>가 복사된 문자열을 가리키는 새 포인터 배열을 작성한다.<ref name="exec">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/exec.c?h=v6.12 Linux v6.12, fs/exec.c], <code>copy_strings()</code>, <code>setup_arg_pages | 호출 당시의 포인터는 이전 프로그램의 주소 공간을 가리킨다. exec는 그 주소 공간을 새 이미지로 교체하므로 포인터 값만 그대로 넘길 수 없다. 커널은 exec 준비 과정에서 문자열을 새 실행을 위한 메모리에 복사하고, 새 사용자 스택의 위치를 정한다. 이어 ELF 로더의 <code>create_elf_tables()</code>가 복사된 문자열을 가리키는 새 포인터 배열을 작성한다.<ref name="exec">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/exec.c?h=v6.12 Linux v6.12, fs/exec.c], <code>copy_strings()</code>, <code>setup_arg_pages()</code>.</ref><ref name="elf">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/binfmt_elf.c?h=v6.12 Linux v6.12, fs/binfmt_elf.c], <code>create_elf_tables()</code>, <code>load_elf_binary()</code>.</ref> | ||
두 아키텍처의 네이티브 64비트 초기 사용자 스택은 낮은 주소부터 <code>argc</code>, <code>argv</code>, <code>envp</code>, 보조 벡터 순서로 읽을 수 있다. 각 포인터 배열은 NULL로 끝난다. 다음 그림은 실제 주소가 증가하는 방향을 아래쪽으로 표시한 개념도이다. | 두 아키텍처의 네이티브 64비트 초기 사용자 스택은 낮은 주소부터 <code>argc</code>, <code>argv</code>, <code>envp</code>, 보조 벡터 순서로 읽을 수 있다. 각 포인터 배열은 NULL로 끝난다. 다음 그림은 실제 주소가 증가하는 방향을 아래쪽으로 표시한 개념도이다. | ||
| 87번째 줄: | 87번째 줄: | ||
</pre> | </pre> | ||
RISC-V의 <code>ret_from_exception</code>은 저장된 <code>status</code>와 <code>epc</code>를 각각 <code>sstatus</code>, <code>sepc</code>에 쓰고 일반 레지스터와 사용자 <code>sp</code>를 복원한 뒤 <code>sret</code>를 실행한다. <code>sret</code>는 <code>SPP=0</code>에 따라 U-mode로 전환하고 <code>sepc</code>의 주소에서 실행을 시작한다. 이때 SIE는 SPIE 값으로 갱신된다. '''스택 포인터를 복원하는 것은 앞의 어셈블리 명령이며, sret 자체가 사용자 스택을 찾아 주는 것은 아니다.'''<ref name="rv-return">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/entry.S?h=v6.12 Linux v6.12, arch/riscv/kernel/entry.S], <code>ret_from_exception | RISC-V의 <code>ret_from_exception</code>은 저장된 <code>status</code>와 <code>epc</code>를 각각 <code>sstatus</code>, <code>sepc</code>에 쓰고 일반 레지스터와 사용자 <code>sp</code>를 복원한 뒤 <code>sret</code>를 실행한다. <code>sret</code>는 <code>SPP=0</code>에 따라 U-mode로 전환하고 <code>sepc</code>의 주소에서 실행을 시작한다. 이때 SIE는 SPIE 값으로 갱신된다. '''스택 포인터를 복원하는 것은 앞의 어셈블리 명령이며, sret 자체가 사용자 스택을 찾아 주는 것은 아니다.'''<ref name="rv-return">[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/riscv/kernel/entry.S?h=v6.12 Linux v6.12, arch/riscv/kernel/entry.S], <code>ret_from_exception</code>.</ref><ref>[https://docs.riscv.org/reference/isa/v20260120/priv/supervisor.html RISC-V Supervisor-Level ISA], <code>sstatus</code>, <code>sepc</code>, SRET.</ref> | ||
x86-64에서는 복귀 어셈블리가 저장된 상태를 복원하고, RIP·CS·RFLAGS·RSP·SS로 이루어진 복귀 프레임을 <code>iretq</code>가 적용하여 CPL 3의 사용자 코드로 나간다. 일반 시스템 콜에는 SYSRET 복귀도 있지만, 여기의 exec가 준비한 새 레지스터 상태는 그 경로의 조건을 만족하지 않아 IRET 경로로 간다.<ref>[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/elf.h?h=v6.12 Linux v6.12, arch/x86/include/asm/elf.h], <code>elf_common_init()</code>; [https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/common.c?h=v6.12 arch/x86/entry/common.c], <code>do_syscall_64()</code>; [https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/entry_64.S?h=v6.12 arch/x86/entry/entry_64.S], 사용자 복귀 경로와 <code>iretq</code>.</ref> | x86-64에서는 복귀 어셈블리가 저장된 상태를 복원하고, RIP·CS·RFLAGS·RSP·SS로 이루어진 복귀 프레임을 <code>iretq</code>가 적용하여 CPL 3의 사용자 코드로 나간다. 일반 시스템 콜에는 SYSRET 복귀도 있지만, 여기의 exec가 준비한 새 레지스터 상태는 그 경로의 조건을 만족하지 않아 IRET 경로로 간다.<ref>[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/elf.h?h=v6.12 Linux v6.12, arch/x86/include/asm/elf.h], <code>elf_common_init()</code>; [https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/common.c?h=v6.12 arch/x86/entry/common.c], <code>do_syscall_64()</code>; [https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/entry_64.S?h=v6.12 arch/x86/entry/entry_64.S], 사용자 복귀 경로와 <code>iretq</code>.</ref> | ||
| 119번째 줄: | 119번째 줄: | ||
<code>e_entry</code>는 보통 C의 <code>main()</code> 주소가 아니다. 일반적인 C 프로그램은 <code>_start</code> 같은 런타임 시작 코드에서 스택의 인자 정보를 읽고 실행 환경을 준비한 뒤 <code>main()</code>을 호출한다. ELF가 <code>_start</code>라는 이름을 강제하는 것도 아니다. 커널은 함수 이름을 찾지 않고 ELF에 기록된 진입 주소를 사용한다. | <code>e_entry</code>는 보통 C의 <code>main()</code> 주소가 아니다. 일반적인 C 프로그램은 <code>_start</code> 같은 런타임 시작 코드에서 스택의 인자 정보를 읽고 실행 환경을 준비한 뒤 <code>main()</code>을 호출한다. ELF가 <code>_start</code>라는 이름을 강제하는 것도 아니다. 커널은 함수 이름을 찾지 않고 ELF에 기록된 진입 주소를 사용한다. | ||
== 동적 링크된 프로그램에서는 무엇이 달라지는가 == | == 동적 링크된 프로그램에서는 무엇이 달라지는가 == | ||