[설명] Pintos Project 2 : Argument Passing
Argument Passing
process_exec()에서 사용자 프로그램을 위한 인자 설정
x86-64 호출 규약 (Calling Covention)
이 섹션은 64bit Unix 시스템에서의 일반적인 함수 호출 방식
즉 x86-64 호출 규약의 핵심적인 사항들을 요약한 것이다.
호출 규약의 동작 방식은 다음과 같다.
➀ 사용자 레벨의 애플리케이션은 함수 인자를 다음 순서의 정수 레지스터에 저장하여 전달한다.
%rdi,%rsi,%rdx,%rcx,%r8,%r9
➁ 호출자(caller)는 다음 명령어의 주소(즉, 리턴할 위치)를 스택에 푸시한 뒤,
피호출자(callee)의 첫 번째 명령어로 점프한다.
- 이 동작은 하나의 x86-64 명령어인
CALL로 처리된다 - 즉,
CALL명령은 자동으로return address를 스택에push한 후 점프한다
➂ 피호출자(callee)가 실행된다.
➃ return 값이 있는 경우, 그 값을 RAX 레지스터에 저장한다.
➄ 피호출자는 스택에서 return 주소를 팝(pop)하고, 그 위치로 점프하면서 함수 실행을 종료한다.
RET명령어가 이 작업을 한다.
예시로 이해하기
1
int f(int a, int b, int c);
위 함수를 다음과 같이 호출할 경우
1
f(1, 2, 3);
호출 당시 피호출자 f()입장에서의 스택 및 레지스터 상태는 다음과 같다.
1
2
3
4
5
6
7
8
+----------------+
stack pointer --> 0x4747fe70 | return address |
+----------------+
레지스터 상태:
RDI: 0x0000000000000001 (a = 1)
RSI: 0x0000000000000002 (b = 2)
RDX: 0x0000000000000003 (c = 3)
호출 직후, 함수 안에서 볼 수 있는 상태이다.
return address는 스택에 저장되어있고, 인자들은 레지스터로 전달되어 있다.
Program Startup Details (프로그램 시작 세부 사항)
Pintos 사용자 프로그램용 C 라이브러리는 lib/user/entry.c에 있는 _start()를 사용자 프로그램의 진입점(entry point)으로 지정한다.
이 함수는 사용자 프로그램을 실행할 때 main()을 호출하고, 종료되면 자동으로 exit()을 호출한다.
1
2
3
4
void
_start (int argc, char *argv[]) {
exit (main (argc, argv));
}
커널은 사용자 프로그램이 실행을 시작하기 전에 초기 함수에 대한 인자들을 레지스터에 넣어야 한다.
이 인자들은 일반적인 호출 규칙과 동일한 방식으로 전달된다.
다음 예시 명령어에 대한 인자들을 처리하는 방법을 고려해봐라.
1
/bin/ls -l foo bar
➀ 명령어를 단어로 나눈다.
/bin/ls,-l,foo,bar
➁ 이 단어들을 스택 맨 상단에 배치한다.
- 순서는 중요하지 않다. (왜냐면 포인터를 통해 참조되기 때문이다)
➂ 각 문자열의 주소와 null pointer sentinel을 오른쪽에서 왼쪽(right-to-left)으로 스택에 push 한다.
- 이들은
argv의 요소들이다 null pointer sentinel은argv[argc]가 null 포인터가 되도록 보장하는데, 이는 C 표준에서 요구된다- 이 순서는
argv[0]이 가장 낮은 가상 주소에 있도록 보장한다 - 워드 정렬 접근이 비정렬 접근보다 더 빠르므로, 최상의 성능을 위해 첫 번째 푸시 전에는 스택 포인터를 8의 배수로 내린다
➃ %rsi레지스터를 argv(=argv[0]의 주소)로 지정하고, %rdi를 argc로 설정한다.
➄ 마지막으로 가짜 리턴 주소를 푸시한다.
- 진입 함수가 실제로 반환되지는 않지만,
- 그 함수의 스택 프레임은 다른 모든 함수들과 동일한 구조를 가져야 한다.
아래 표는 사용자 프로그램이 시작되기 바로 직전 스택과 관련 레지스터의 상태를 보여준다.
스택은 아래 방향으로 성장(grow down)한다는 것에 유의해라!
| Address | Name | Data | Type |
|---|---|---|---|
| 0x4747fffc | argv[3][…] | ‘bar\0’ | char[4] |
| 0x4747fff8 | argv[2][…] | ‘foo\0’ | char[4] |
| 0x4747fff5 | argv[1][…] | ‘-l\0’ | char[3] |
| 0x4747ffed | argv[0][…] | ‘/bin/ls\0’ | char[8] |
| 0x4747ffe8 | word-align | 0 | uint8_t[] |
| 0x4747ffe0 | argv[4] | 0 | char * |
| 0x4747ffd8 | argv[3] | 0x4747fffc | char * |
| 0x4747ffd0 | argv[2] | 0x4747fff8 | char * |
| 0x4747ffc8 | argv[1] | 0x4747fff5 | char * |
| 0x4747ffc0 | argv[0] | 0x4747ffed | char * |
| 0x4747ffb8 | return address | 0 | void (*) () |
| 레지스터 | 값 |
|---|---|
| RDI | 4 (argc) |
| RSI | 0x4747ffc0 (argv의 시작 주소) |
이 예제에서 스택 포인터는 0x4747ffb8로 초기화된다.
코드에서는 include/threads/vaddr.h에 정의된 USER_STACK에서 스택을 시작해야 한다.
인자 전달 코드를 디버깅 할 때는 <stdio.h>에 선언된 비표준 함수(hex_dump())가 유용할 수 있다.
Implement the argument passing - 인자 전달을 구현해라
현재는 process_exec()는 새 프로세스에 인자를 전달하는 기능을 지원하지 않는다.
이 기능을 구현하려면, process_exec()가 단순히 프로그램 이름만 받는 대신,
공백을 기준으로 문자열을 나눠 각 단어로 구분하도록 확장해야 한다.
예 : process_exec("grep foo bar")
grep은 실행할 프로그램, foo, bar은 프로그램에 전달할 인자들이다.
첫 번째 단어는 프로그램 이름이고, 두 번째 단어부터는 첫 번째 인자, 그 다음은 두 번째 인자, 이런식이다.
예 : argv[0] = "grep", argv[1] = "foo", argv[2] = "bar", argc = 3
다시 말해, process_exec("grep foo bar")는 "grep"을 실행하면서 "foo"와 "bar"라는 두 개의 인자를 전달해야 한다.
사용자는 단순 문자열을 입력하지만, 내부적으로는 문자열을 잘라서 프로그램과 인자들을 구분하고, 이들을 스택에 적절히 배치한 후 실행되도록 만들어야 한다.
명령어 줄 안에서 여러 개의 공백은 하나의 공백과 동일하게 처리되어야 하므로,
process_exec("grep foo bar")는 기존 예제와 동일하게 간주된다.
명령어 줄 인자 길이에 대해서는 합리적인 제한을 둘 수 있다.
예를 들어, 인자들이 한 페이지(4KB) 안에 들어오도록 제한할 수 있다.
(별도로, pintos 유틸리티가 커널에 전달할 수 있는 명령줄 인자에는 128 byte 제한이 있다.)
인자 문자열은 원하는 방식으로 파싱해도 된다.
구현이 어려울 경우, strtok_r()를 참고해라.
이 함수는 include/lib/string.h에 정의되어있고, lib/string.c에 자세한 주석과 함께 구현되어 있다.
더 자세한 정보는 man strtok_r 명령어를 통해 확인할 수 있다.
(man 명령어로 함수 설명서를 볼 수 있다.)
strtok_r()
이 함수는 문자열을 구분자를 기준으로 토큰(부분 문자열)으로 나누는 재진입 가능 토크나이저(tokenizer)이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
char *
strtok_r (char *s, const char *delimiters, char **save_ptr) {
char *token; // 토큰의 시작 위치를 저장할 포인터
ASSERT (delimiters != NULL);
ASSERT (save_ptr != NULL);
/* If S is nonnull, start from it.
If S is null, start from saved position. */
if (s == NULL)
s = *save_ptr;
ASSERT (s != NULL);
/* 현재 위치(*s)가 구분자에 속하면 넘김 (연속된 구분자를 건너띔)
만약 \0까지 도달했다면, 남은 토큰이 없음로 NULL을 반환함 */
while (strchr (delimiters, *s) != NULL) {
if (*s == '\0') {
*save_ptr = s;
return NULL;
}
s++;
}
token = s; // 토큰의 시작 위치를 저장
while (strchr (delimiters, *s) == NULL)
s++;
if (*s != '\0') {
*s = '\0';
*save_ptr = s + 1;
} else
*save_ptr = s;
return token;
}
strtok_r()는 문자열을 주어진 구분자 기준으로 안전하게 분할 가능한 함수다- 상태를
save_ptr에 저장하므로 여러번 호출해도 안정적이다 - 문자열을 직접 수정하며(
\0으로 끊음), 리턴 값은 각 토큰의 시작 위치이다 - 공백이 연속돼도 문제 없이 건너띈다