돌고돌아 포인터
포인터 기본 개념
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 함수처럼 인자를 받을 수 있다는 게 무슨 말인가요?


