Search

Argument Passing

돌고돌아 포인터

포인터 기본 개념

int x = 42; // x는 값 42를 저장 int* ptr = &x; // // ptr은 x의 주소를 저장 printf("%d", *ptr); // *ptr로 x의 값(42)에 접근
C
복사
포인터 사용 이유
메모리 주소 조작 : 스택 특정 위치에 값 저장
간접 접근 : 주소를 통해 실제 데이터에 접근
동적 메모리 : 런타임에 메모리 위치 결정

8byte와 타입의 관계

char // 1바이트 int // 4바이트 char* // 8바이트 (포인터는 주소를 저장) char** // 8바이트 (포인터의 주소를 저장) char*** // 8바이트 (포인터의 포인터의 주소를 저장)
C
복사
8바이트인 이유
64비트 시스템: 메모리 주소가 64비트 = 8바이트
포인터 크기: 모든 포인터는 메모리 주소이므로 8바이트

각 단계별 포인터 연산 분석

Step2 : 문자열 복사

char* argv_addresses[argc]; // 문자열 주소들을 저장할 배열 for(int i = argc - 1; i >= 0; i--) { size_t arg_len = strlen(argv[i]) + 1; stack_ptr -= arg_len; // 스택 포인터를 문자열 크기만큼 이동 memcpy(stack_ptr, argv[i], arg_len); // 문자열 복사 argv_addresses[i] = stack_ptr; // 복사된 문자열의 주소 저장 }
C
복사

Step4 : NULL 포인터 추가

stack_ptr -= sizeof(char*); // 8바이트만큼 공간 확보 *(char**)stack_ptr = NULL; // NULL 포인터 저장
C
복사

Step5 : argv 포인터들 저장

for(int i = 0; i < argc; i++) { stack_ptr -= sizeof(char *); // 8바이트 공간 확보 *(char **)stack_ptr = argv_addresses[i]; // 문자열 주소 저장 }
C
복사

Step6 : argv 주소 저장

char **argv_ptr = (char **)stack_ptr; // argv 배열의 시작 주소 stack_ptr -= sizeof(char **); // 8바이트 공간 확보 *(char ***)stack_ptr = argv_ptr; // argv 주소 저장
C
복사

메모리 레이아웃 시각화

예시: "program arg1 arg2" 메모리 주소 저장된 값 타입 용도 ──────────────────────────────────────────────────────── 0x1000: "program\0" char[] 실제 문자열 0x1008: "arg1\0" char[] 실제 문자열 0x1010: "arg2\0" char[] 실제 문자열 0x2000: NULL char* argv[3] = NULL 0x2008: 0x1010 char* argv[2]"arg2" 0x2010: 0x1008 char* argv[1]"arg1" 0x2018: 0x1000 char* argv[0]"program" 0x3000: 0x2008 char** argv 배열 주소 0x3008: 3 int argc 0x3010: 0 void* 가짜 반환 주소
C
복사

타입 캐스팅 이유

포인터 산술 & 타입 안정성
char *ptr; ptr--; // 1바이트씩 이동 char **ptr2; ptr2--; // 8바이트씩 이동 (포인터 크기)
C
복사
값 저장 시 타입 매칭
// 잘못된 방법: *stack_ptr = argv_ptr; // 컴파일 에러! : 포인터(주소값)를, 주소값에 해당하는 공간의 값으로 할당하려고 하니 에러 발생 // 올바른 방법: *(char***)stack_ptr = argv_ptr; // 타입 매칭
C
복사

예시

int main(int argc, char **argv) { // argc = 3 (스택에서 읽음) // argv = 0x2008 (스택에서 읽음) printf("%s\n", argv[0]); // argv[0] = *(argv + 0) = *(0x2008) = 0x1000 → "program" printf("%s\n", argv[1]); // argv[1] = *(argv + 1) = *(0x2010) = 0x1008 → "arg1" printf("%s\n", argv[2]); // argv[2] = *(argv + 2) = *(0x2018) = 0x1010 → "arg2" }
C
복사

실행 흐름

// 👇👇👇 TCB(Thread Control Block) struct thread { /* Owned by thread.c. */ tid_t tid; /* Thread identifier. */ enum thread_status status; /* 스레드 상태 : 준비, 실행, 대기 + (생성, 종료) */ char name[16]; /* Name (for debugging purposes). */ int priority; /* Priority. */ int64_t wakeup_tick; /* 깨워야 할 tick */ int base_priority; // 기존 우선순위 struct lock* waiting_lock; // 대기중인 lock struct list_elem donation_elem; // 내가 다른 스레드의 donation_list에 들어갈 때 쓰이는 원소 struct list donation_list; // 나에게 donation해준 스레드들의 리스트 int nice; // nice 값 int64_t recent_cpu; // recent_cpu 값 struct list_elem all_elem; // all_list에 들어갈 때 쓰이는 원소 /* Shared between thread.c and synch.c. */ struct list_elem elem; /* List element. */ #ifdef USERPROG /* Owned by userprog/process.c. */ uint64_t *pml4; /* Page map level 4 */ #endif #ifdef VM /* Table for whole virtual memory owned by thread. */ struct supplemental_page_table spt; #endif /* Owned by thread.c. */ // 👇👇👇 컨텍스트 스위칭을 위한 레지스터 저장소 : 스레드가 중단될 때 모든 CPU 레지스터 값을 저장 struct intr_frame tf; /* Information for switching */ // 👆👆👆 컨텍스트 스위칭을 위한 레지스터 저장소 : 스레드가 중단될 때 모든 CPU 레지스터 값을 저장 unsigned magic; /* Detects stack overflow. */ }; // 👆👆👆 TCB(Thread Control Block)
C
복사
윗 코드에서 이번에 집중할 부분은 다음이지 않나 싶습니다 :
1.
struct thread 는 TCB(Thread Control Block)이라는 점
TCB나 PCB는 운영체제 공부하다보면 마주치게 되는 단어라고 생각합니다. 프로그램을 실행하면 커널 영역에는 PCB(TCB)라는 정보가 저장되고 사용자 영역에는 프로세스가 코드 & 데이터 & 힙 & 스택 영역으로 나뉘어 저장됩니다.
운영체제가 해당 프로세스(스레드)를 실행할 수 있도록 스케줄링해서 실행해야하는 시점에, 프로세스(스레드)는 어느 레지스터 값을 사용하고 있었고 메모리 내 어디에 적재가 되어있었고 ID는 어떻게 되는 지 등을 저장하고 있어야 CPU가 그 내용들을 읽어 그 지점부터 재개하거나 처음 실행하거나 할 것입니다. (이 과정을 컨텍스트 스위칭(문맥 교환)이라고 합니다)
스레드에서 다른 스레드로 전환되는 과정에서 이 Thread 구조체 내 정보들(tid, state, tf, name, priority)을 기반으로 CPU 저장장치들의 정보를 갱신하고 실행해나가는거죠.
2.
struct intr_frame tf 가 모든 CPU 레지스터 값들을 저장한다는 점
이 구조체는 다음 내용을 포함합니다 :
struct intr_frame { /* Pushed by intr_entry in intr-stubs.S. These are the interrupted task's saved registers. */ // 범용 제지스터들 struct gp_registers R; // rax, rbx, rcx, rdx, rbp, rdi, rsi, r8~r15 uint16_t es; uint16_t __pad1; uint32_t __pad2; uint16_t ds; uint16_t __pad3; uint32_t __pad4; /* Pushed by intrNN_stub in intr-stubs.S. */ uint64_t vec_no; /* Interrupt vector number. */ /* Sometimes pushed by the CPU, otherwise for consistency pushed as 0 by intrNN_stub. The CPU puts it just under `eip', but we move it here. */ uint64_t error_code; /* Pushed by the CPU. These are the interrupted task's saved registers. */ uintptr_t rip; uint16_t cs; uint16_t __pad5; uint32_t __pad6; uint64_t eflags; uintptr_t rsp; uint16_t ss; uint16_t __pad7; uint32_t __pad8; } __attribute__((packed)); /* Interrupt stack frame. */ struct gp_registers { uint64_t r15; uint64_t r14; uint64_t r13; uint64_t r12; uint64_t r11; uint64_t r10; uint64_t r9; uint64_t r8; uint64_t rsi; uint64_t rdi; uint64_t rbp; uint64_t rdx; uint64_t rcx; uint64_t rbx; uint64_t rax; } __attribute__((packed));
C
복사
이 구조체(intr_frame)는 한 스레드의 여러 정보들을 가지고 있습니다. vetor_no(인터럽트 발생 시에 어떤 인터럽트 벡터에 의해 호출되었는지 나타내는 번호), R(15개의 범용 레지스터들), rsp(스택 포인터) 등의 여러 값들을 가지고 있음을 발견할 수 있습니다.
이런 다양한 내용들을 이용해 실행할 스레드의 중간 정보들을 기반으로 CPU가 그 지점부터 실행해나갑니다.

구현 코드

process_exec()

process_exec() 함수가 어떤 역할의 함수인지를 명확히 알지 못한다면 모든 작업에서 왜?라는 질문에 대해 답할 수 없을 거라고 생각합니다. 이 함수는 현재의 실행 문맥을 인자의 f_name 으로 변경합니다. 로직의 흐름을 따라 더 자세히 설명해보자면 다음과 같습니다 :
1.
인터럽트 프레임(새 문맥을 위한 정보 구조체)의 내용을 초기화합니다.
2.
기존 프로세스의 자원을 모두 정리합니다.
3.
터미널에 입력한 명령어를 파일 이름과 인자들로 분할합니다.
4.
새 프로그램을 로드합니다.
5.
명령어의 인자들을 인터럽트 프레임의 여러 부분들로 설정합니다.
6.
사용자 모드로 전환(새 프로그램으로 영구 전환)합니다.

setup_arguments()

위 함수의 여러 단계 중 5단계에 해당하는 내용을 수행하는 함수입니다.
x86-64 시스템 콜/프로그램 실행 시의 스택 레이아웃을 "System V AMD64 ABI" 호출 규약에 맞춰서 구성하는 함수입니다(호출 규약이 뭔 지는 알 필요 없다고 생각하고 스택에 어떤 식으로 쌓이는 지만 파악하면 됩니다).
스택은 아래로 성장한다는 것을 기억할 것
호출 규약에 맞춰 스택 레이아웃을 구성합니다 :
1.
인자 문자열들을 스택에 복사
2.
각 인자에 대한 포인터 배열(argv), 인자 개수(argc), NULL, 가짜 반환 주소 등을 정해진 순서정렬(8바이트 단위)에 맞게 스택에 배치
이를 통해, 새로 실행되는 사용자 프로그램이 표준 C main 함수처럼 int main(int argc, char *argv[]) 형태로 인자를 받을 수 있습니다.
Q) 사용자 프로그램이 표준 C main 함수처럼 인자를 받을 수 있다는 게 무슨 말인가요?