문자열 Passing하기
요약한 그림
pintos의 프로그램 실행 모델
esp : 스택 포인터 주소
eip : text 세그먼트 시작 주소
Argument Passing 흐름
프로그램 실행과 스택을 통한 인자 전달
유저 스택에 인자 삽입
주어진 ‘file_name’ 문자열을 사용하여 새로운 스레드 생성
코드
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
fn_copy = palloc_get_page (0); // 페이지 크기의 메모리 블록을 할당. 0(기본)
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE); // 문자열을 안전하게 복사.
// 문자열을 공백을 기준으로 파싱하여 첫번째 토큰 추출
char *save_ptr;
strtok_r(file_name, " ", &save_ptr);
// 새 스레드 생성(첫 번째 토큰 보내기)
tid = thread_create(file_name, PRI_DEFAULT, initd, fn_copy);
// 오류 처리
if(tid == TID_ERROR)
palloc_free_page(fn_copy);
return tid;
}
설명
저장된 실행 파일을 현재 프로세스에 로드하고 실행하는 역할
코드
int
process_exec (void *f_name) {
char *file_name = f_name;
bool success;
/* We cannot use the intr_frame in the thread structure.
* This is because when current thread rescheduled,
* it stores the execution information to the member. */
struct intr_frame _if;
_if.ds = _if.es = _if.ss = SEL_UDSEG;
_if.cs = SEL_UCSEG;
_if.eflags = FLAG_IF | FLAG_MBS;
/* We first kill the current context */
// 현재 컨텍스트 종료
process_cleanup ();
// 변수 초기화
int count = 0;
char *parse[128];
char *token;
char *save_ptr;
// 인자 파싱
for (token = strtok_r(file_name, " ", &save_ptr); token != NULL;
token = strtok_r(NULL, " ", &save_ptr))
{
parse[count] = token;
count++;
}
/* And then load the binary */
/* if_.esp는 스택 포인터
바이너리 로드 - 'file_name'에 지정된 프로그램을 메모리에 적재한다.
load - '_if'에 초기 스택 포인터와 엔트리 포인트(프로그램의 시작주소) 설정 */
success = load (file_name, &_if); // load() : file_name의 프로그램을 메모리에 적재
// 로드 실패 처리
if (!success)
{
palloc_free_page(file_name);
return -1;
}
// 유저 프로그램이 실행 되기 전에 argument_stack() 함수 호출하여 스택에 인자 저장.
argument_stack(parse, count , &_if.rsp);
// 디버깅 툴 - 메모리 내용을 16진수로 화면에 출력. 유저 스택에 인자를 저장 후 유저 스택 메모리 확인
hex_dump(_if.rsp , _if.rsp , USER_STACK-(uint64_t)_if.rsp , true);
/* Start switched process. */
/* If load failed, quit. */
palloc_free_page (file_name);
do_iret (&_if); // 인터럽트 반환 명령을 사용하여 새로운 사용자 모드 프로그램 실행
NOT_REACHED ();
}
설명
parse
배열에 저장합니다.file_name
에 지정된 프로그램을 메모리에 적재합니다.do_iret
를 사용하여 새로운 사용자 모드 프로그램을 실행합니다.proces_exec() 함수에서 parsing한 프로그램 이름 and 인자를 스택에 저장
코드
void
argument_stack(char **parse, int count, void **rsp) // 주소를 전달 받았으니 이중 포인터
{
char *stack_ptr = (char *)*rsp; // 현재 스택 포인터를 가리키는 포인터
uintptr_t addr[count]; // 각 문자열의 주소 저장.
/*
1. 프로그램 이름 및 인자(문자열) push
스택은 아래 방향으로 성장하므로 스택에 인자 추가시 string을 오른쪽 > 왼쪽(역방향) push
'memcpy'를 사용하여 문자열을 스택에 복사.
*/
for (int i = count - 1; i >= 0; i--) {
int len = strlen(parse[i]) + 1; // NULL 포함
stack_ptr -= len;
memcpy(stack_ptr, parse[i], len);
addr[i] = (uintptr_t)stack_ptr;
}
/*
2. 정렬 패딩 push
각 문자열 push 후 (8)byte 단위로 정렬하기 위해 필요한 만큼 padding 추가.
*/
while ((uintptr_t)stack_ptr % 8 != 0) {
stack_ptr--;
*stack_ptr = 0;
}
/*
3. 프로그램 이름 및 인자 주소들 push
각 문자열의 주소를 스택에 역순으로 저장. 마지막에 NULL 포인터 추가하여 문자열 배열의 끝 표시
*/
for (int i = count; i >= 0; i--) {
stack_ptr -= sizeof(uintptr_t);
*(uintptr_t *)stack_ptr = (i == count) ? 0 : addr[i]; // 마지막은 NULL 포인터
}
/*
4. argv (문자열을 가리키는 주소들의 배열을 가리킴) push
이 주소는 프로그램이 실행될 때 인자를 참조하는데 사용
*/
uintptr_t argv = (uintptr_t)stack_ptr;
stack_ptr -= sizeof(uintptr_t);
*(uintptr_t *)stack_ptr = argv;
// 5. argc (문자열의 개수 저장) push
stack_ptr -= sizeof(int);
*(int *)stack_ptr = count;
/*
6. fake address(0) 저장
다음 인스트럭션 주소를 push 해야 하는데, 지금은 프로세스를 생성하는 거라서 반환 주소x
*/
stack_ptr -= sizeof(void *);
*(void **)stack_ptr = 0;
// 최종 스택 포인터 설정.
*rsp = (void *)stack_ptr;
}
설명
memcpy
를 사용하여 parse[i]
값을 stack_ptr
에 복사합니다.stack_ptr
)가 8로 나누어 떨어질 때까지 0
을 채워 정렬합니다.argv
(문자열을 가리키는 주소들의 배열을 가리킴) push:argc
(문자열의 개수 저장) push:argc
로 사용됩니다.rsp
)에 저장하여 함수 호출자가 이를 참조할 수 있도록 합니다.ex)
스택 포인터: 0x80000000
==========================
| 문자열 | 스택 주소 |
==========================
| "arg2\0" | 0x7FFFFFF8 |
| "arg1\0" | 0x7FFFFFE8 |
| "program_name\0" | 0x7FFFFFD8 |
==========================
스택 포인터: 0x7FFFFFD8
==========================
| 문자열 시작 주소 | 스택 주소 |
==========================
| NULL | 0x7FFFFFD0 |
| 0x7FFFFFF8 (arg2) | 0x7FFFFFC8 |
| 0x7FFFFFE8 (arg1) | 0x7FFFFFC0 |
| 0x7FFFFFD8 (program_name) | 0x7FFFFFB8 |
==========================
스택 포인터: 0x7FFFFFB8
==========================
| `argv` 주소 | 스택 주소 |
==========================
| 0x7FFFFFB8 | 0x7FFFFFB0 |
==========================
스택 포인터: 0x7FFFFFB0
==========================
| `argc` 값 | 스택 주소 |
==========================
| 3 | 0x7FFFFFA8 |
==========================
스택 포인터: 0x7FFFFFA8
==========================
| 가짜 리턴 주소 | 스택 주소 |
==========================
| 0 | 0x7FFFFFA0 |
==========================
| 주소 | 값 |
|------------|----------------------------|
| 0x7FFFFFA0 | 0 | (가짜 리턴 주소)
| 0x7FFFFFA8 | 3 | (argc)
| 0x7FFFFFB0 | 0x7FFFFFB8 | (argv 배열의 시작 주소)
| 0x7FFFFFB8 | 0x7FFFFFD8 | (program_name의 주소)
| 0x7FFFFFC0 | 0x7FFFFFE8 | (arg1의 주소)
| 0x7FFFFFC8 | 0x7FFFFFF8 | (arg2의 주소)
| 0x7FFFFFD0 | 0 | (NULL 포인터)
| 0x7FFFFFD8 | "program_name\0" | (프로그램 이름)
| 0x7FFFFFE8 | "arg1\0" | (첫 번째 인자)
| 0x7FFFFFF8 | "arg2\0" | (두 번째 인자)
| 0x80000000 | | (초기 스택 포인터)
이 그림은 각 단계에서 스택이 어떻게 변경되는지 보여줍니다. 프로그램이 실행될 때, 스택에서 이 데이터를 참조하여 argc
와 argv
를 통해 인자들을 올바르게 처리할 수 있습니다.