x86-64 calling convention
calling convention의 규칙 중 하나는 함수 인수와 반환값이 전달되는 방법을 제어한다. x86-64 Linux에서 처음 6개의 함수 인수(function arguments)들은 각각 %rdi
, %rsi
, %rdx
, %rcx
, %r8
, %r9
레지스터에 전달된다. 일곱 번째 인자와 그 이후의 인수는 stack에 전달된다. 그리고 반환값은 %rax
레지스터에 전달된다.
전체 규칙은 이것보다 더 복잡하다. 몇 가지 주요 사항들을 살펴보자.
- single machine word(64 bits/8 bytes)에 맞는 structure argument는 single register에 전달됨
- Example:
struct small { char a1, a2; }
- 2~4 machine words(16–32 bytes)에 해당하는 structure는 sequential registers에 전달된다. 그들이 multiple arguments인 것처럼
- Example:
struct medium { long a1, a2; }
- structure가 4 machine words보다 큰 경우는 항상 stack에 전달된다.
- Example:
struct large { long a, b, c, d, e, f, g; }
- 부동소수점 인수는 일반적으로 “SSE registers”라는 레지스터에 전달되는데 우리는 깊이 알아보지는 않을 것이다.
- return 값이 8byte 이상이면 caller는 return value을 위한 공간을 예약하고 함수의 첫 번째 인수로 해당 공간의 주소를 전달한다. callee는 return할 때 공간을 채운다.
Program startup Details
user program이 실행될 때, 아래와 같은 모습으로 실행된다.
// lib/user/entry.c
// user program의 진입점
void _start(int argc, char *argv[])
{
exit(main(argc, argv));
}
여기에는 argc와 argv가 필요하다. 그럼 command line에서 argc와 argv를 얻어와야 한다. 이걸 우리가 구현하는 것이다.
example command: /bin/ls -l foo bar
.
이게 들어왔을 때 어떻게 argument를 다룰 것인가?
/bin/ls
,l
,foo
,bar
으로 쪼갠다.- stack의 꼭대기에 배치한다.(순서는 상관없다 - pointer로 참조할거니까)
- stack에 오른쪽부터 왼쪽 순서로 push한다. 이들은 argv의 요소들이다.
argv[argc]
은 null pointer이다(C standard) .argv[0]
는 가장 낮은 가상 주소에 있도록 한다 . Word-aligned 접근이 unaligned 접근보다 빠르다. 따라서 최상의 성능을 위해 스택 포인터를 첫 번째 push 전에 8의 배수로 반올림하라. - Point
%rsi
toargv
(the address ofargv[0]
) and set%rdi
toargc
.- argv: main 함수가 받은 각각의 인자들
- argc: main함수가 받은 인자의 수
- 마지막으로, a fake "return address"를 push한다: entry function은 return되지 않지만 그것의 stack frame은 다른 것과 동일한 구조를 가져야 한다.
user program이 시작하기 직전의 stack과 register 상태. stack이 아래로 자라는 것을 눈여겨 볼 필요가 있다.
RDI(
%rdi
): 4 | RSI(%rsi
): 0x4747ffc0
hex_dump()
로 argument passing이 어떻게 stack에 쌓이는지 볼 수 있다.
Implement the argument passing
현재 process_exec()
는 새 프로세스에 인수 전달을 지원하지 않는다. process_exec()
를 확장 하여 이 기능을 구현하라.
첫 번째 단어는 프로그램 이름이고 두 번째 단어는 첫 번째 인수 등이다.
즉, process_exec("grep foo bar")
두 개의 인수 foo 및 bar를 전달하는 grep을 실행해야 한다.
명령줄 내에서 여러 공백은 단일 공백과 동일하므로 process_exec("grep foo bar")
원래 예와 동일하다. 명령줄 인수의 길이에 적절한 제한을 둘 수 있다. 예를 들어 인수를 단일 페이지(4kB)에 맞는 인수로 제한할 수 있다. (There is an unrelated limit of 128 bytes on command-line arguments that the pintos utility can pass to the kernel)
If you're lost, look at strtok_r()
, prototyped in include/lib/string.h
and implemented with thorough comments in lib/string.c
.
구현
구현을 할 때 크게 두 부분으로 나눠서 구현한다.
- 받은 문자열을 parsing후 argv, argc에 저장
- stack에 인자 넣기
흐름
user program을 실행시키는 전체적인 흐름은 다음과 같다.
user program을 실행시키는 경우,
threads.init.c
안에 있는
main()
함수에서
// threads/init.c
/* Pintos main program. */
int main(void)
{
...
/* Break command line into arguments and parse options. */
argv = read_command_line(); // command_line을 읽는다
argv = parse_options(argv); // 해당 line의 option 들을 parsing하여 argv에 담는다.
...
/* Run actions specified on kernel command line. */
run_actions(argv); // argv를 여기서 실행시킴
...
}
argv command line의 option을 parsing하고, argv에 저장한 후
run_actions(argv)
를 실행시킨다.
static void
run_actions(char **argv)
{
/* An action. */
struct action
{
char *name; /* Action name. */
int argc; /* # of args, including action name. */
void (*function)(char **argv); /* Function to execute action. */
};
/* Table of supported actions. */
static const struct action actions[] = {
{"run", 2, run_task}, // action: run, argc: 2, function to execute action: run_task
...
}
run_actions(argv)
함수 안에서
command line에서 run option을 준 경우, run_task
를 실행시킨다.
run_task(char **argv)
안에서
// threads/init.c
/* Runs the task specified in ARGV[1]. */
static void
run_task(char **argv)
{
const char *task = argv[1]; // argv[0]는 run이고 argv[1]부터 filename 시작되는 문자열
printf("Executing '%s':\n", task);
#ifdef USERPROG
if (thread_tests)
{
run_test(task);
}
else
{
process_wait(process_create_initd(task));
}
#else
run_test(task);
#endif
printf("Execution of '%s' complete.\n", task);
}
process_create_initd(task)
라는 함수를 실행시키고, 이때 인자로 받는 task
는 command line에서 run 뒤에 썼던 문자열 전체이다.
만약
pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'
명령어를 사용했다면, task
는 args-single onearg
가 된다.
process_create_initd()
함수에서
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
/* Create a new thread to execute FILE_NAME. */
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
thread_create()
을 실행시키는데, 이때 생성되는 thread는 fn_copy
를 인자로 받는 initd
라는 함수를 실행시킨다. thread_create()
함수에서 initd
라는 함수를 fn_copy
라는 인자를 들고 들어가서 실행시킨다. fn_copy
는 file_name의 복사본 즉, args-single onearg
이다.
/* A thread function that launches first user process. */
static void
initd (void *f_name) {
#ifdef VM
supplemental_page_table_init (&thread_current ()->spt);
#endif
process_init ();
if (process_exec (f_name) < 0)
PANIC("Fail to launch initd\n");
NOT_REACHED ();
}
initd
안에 process_exec()
라는 함수가 있는데 이 함수 안에서 드디어! argument parsing을 진행해주면 된다. 여기서의 f_name
은 앞서 살펴본 args_single onearg
이다.
1. 받은 문자열을 parsing 후 argv, argc에 저장
// userprog/process.c
int process_exec(void *f_name)
{
char *file_name = f_name;
bool success;
struct thread *cur = thread_current();
//intr_frame 권한설정
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();
// for argument parsing
char *argv[64]; // argument 배열
int argc = 0; // argument 개수
char *token;
char *save_ptr; // 분리된 문자열 중 남는 부분의 시작주소
token = strtok_r(file_name, " ", &save_ptr);
while (token != NULL)
{
argv[argc] = token;
token = strtok_r(NULL, " ", &save_ptr);
argc++;
}
/* And then load the binary */
success = load(file_name, &_if);
...
}
process_exec()에서 인자로 받아온 f_name
은 args_single onearg
이다.
이걸 분리해주고, argument 개수는 argc에, argument 인자는 argv 배열에 넣는다. 그 후 memory에 load한다.
2. stack에 인자 넣기
int process_exec(void *f_name)
{
...
/* And then load the binary */
success = load(file_name, &_if);
/* If load failed, quit. */
if (!success)
{
palloc_free_page(file_name);
return -1;
}
// 스택에 인자 넣기
void **rspp = &_if.rsp;
argument_stack(argv, argc, rspp);
_if.R.rdi = argc;
_if.R.rsi = (uint64_t)*rspp + sizeof(void *);
hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)*rspp, true);
palloc_free_page(file_name);
/* Start switched process. */
do_iret(&_if);
NOT_REACHED();
}
여기서 hex_dump는 지금 현재 상황을 화면에 보여주는 함수이다. 화면에 정상적으로 출력하기 위해서는 process_wait() 에서 바로 종료되지 않게 기다려줘야 한다.
int process_wait(tid_t child_tid UNUSED)
{
// for simple tests
for (int i = 0; i < 100000000; i++)
{
}
return -1;
}
argument stack이라는 함수를 만들어 준다.
void argument_stack(char **argv, int argc, void **rsp)
{
// Save argument strings (character by character)
for (int i = argc - 1; i >= 0; i--)
{
int argv_len = strlen(argv[i]);
for (int j = argv_len; j >= 0; j--)
{
char argv_char = argv[i][j];
(*rsp)--;
**(char **)rsp = argv_char; // 1 byte
}
argv[i] = *(char **)rsp; // 배열에 rsp 주소 넣기
}
// Word-align padding
int pad = (int)*rsp % 8;
for (int k = 0; k < pad; k++)
{
(*rsp)--;
**(uint8_t **)rsp = 0;
}
// Pointers to the argument strings
(*rsp) -= 8;
**(char ***)rsp = 0;
for (int i = argc - 1; i >= 0; i--)
{
(*rsp) -= 8;
**(char ***)rsp = argv[i];
}
// Return address
(*rsp) -= 8;
**(void ***)rsp = 0;
}
참고) 잘 몰랐던 함수 정리
memset 함수
void* memset(void* ptr, int value, size_t num);
첫 번째 인자 void * ptr은 세팅하고자 하는 메모리의 시작주소
두 번째 인자 value는 메모리에 세팅하고자 하는 값
세 번째 인자 size_t num은 길이를 뜻한다.
returns 성공시 첫 번째 인자로 들어간 ptr, 실패시 NULL
/* Clear BSS */
static void
bss_init (void) {
/* The "BSS" is a segment that should be initialized to zeros.
It isn't actually stored on disk or zeroed by the kernel
loader, so we have to zero it ourselves.
The start and end of the BSS segment is recorded by the
linker as _start_bss and _end_bss. See kernel.lds. */
extern char _start_bss, _end_bss;
memset (&_start_bss, 0, &_end_bss - &_start_bss);
}
//&_start_bss 부터 길이만큼 0으로 초기화한다.
strlcpy 함수
#include <string.h>
size_t strlcpy(char * dest, const char * src, size_t size);
- dest : 복사가 진행될 목적지
- src : 우리가 복사를 해야하는 값이 들어있는 포인터
size : 최대 size -1 만큼만 복사 진행(”\0값을 넣기 위해”)
strchr 함수
#include <string.h>
const char* strchr(const char* str, int character);
char* strchr(char* str, int character);
문자열에서 특정한 문자가 가장 먼저 나타나는 곳의 위치를 찾고
그 위치를 가리키는 포인터를 리턴한다. 이 때 마지막 NULL 문자도 C 문자열의 일부로 간주하기 때문에 이 함수는 문자열의 맨 끝 부분을 가리키는 포인터를 얻기 위해 사용할 수도 있다.
strlen 함수
size_t strlen(const char* str);
strlen 함수는 char* 타입의 문자열을 받아서 해당 문자열의 길이를 반환하는 함수이다.
char*가 가리키는 주소부터 시작해서 ‘\0’이 나올때까지 문자의 개수를 세고 ‘\0’이 나오면 종료.
memcpy 함수
#include <string.h>
void* memcpy (void* dest, const void* source, size_t num)
메모리의 값을 복사하는 기능을 하는 함수
void* dest : 복사 받을 메모리를 가리키는 포인터
const void* source : 복사할 메모리를 가리키고 있는 포인터
size_t num : 복사할 데이터(값)의 길이(byte 단위)
memcpy(복사받을 메모리, 복사할 메모리, 길이)
결과
/userprog/build에서 다음 명령어를 치면...!
pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'
참고자료
Assembly 2: Calling convention
한양대학교 PPT [핀토스 운영체제 실습] - 원유집