[05.27/week11]Args Passing

CHO WanGi·2025년 5월 27일

KRAFTON JUNGLE 8th

목록 보기
59/89

그동안 TIL 이 뜸했죠...?
PintOS에 딥다이브 하느라 잠자기 바빴습니다...

요구사항

process_exec() 에 Argument Passing 구현

x86-64 Calling Convention(함수 호출 규약)

  1. 사용자 APP, 정수인자 전달 목적
    1. %rdi, %rsi, %rdx, %rcx, %r8, %r9 레지스터를 순서대로 사용
  2. 호출자(caller), 자신의 다음 명령어 주소 stack에 Push & 피 호출자(callee)의 첫번째 명령어로 점프
    1. CALL 이 둘다 수행
  3. 피호출자 실행
  4. 피호출자에게 값이 있다면 그 값을 RAX 레지스터에 저장
  5. 피호출자, RET 명령어 사용하여 스택에서 반환주소 POP
    1. 해당 주소가 지정하는 위치로 점프하면서 반환
  • Ex. f(1, 2,3) 호출시
                                 +----------------+
    스택 포인터 --> 0x4747fe70 |    반환 주소     |
                                 +----------------+
    
    RDI: 0x0000000000000001 | RSI: 0x0000000000000002 | RDX: 0x0000000000000003

요약하자면 저기 레지스터 순서를 잘 알아놓고 일단 넘어가자

Ex. /bin/ls -l foo bar

https://casys-kaist.github.io/pintos-kaist/project2/argument_passing.html

  1. 명령어를 단어들로 나누기 /bin/ls, -l, foo, bar
  2. 단어들(string 자체) Stack 상단에 배치, 포인터를 통해 참조 예정, 순서 상관 X
  3. 각 문자열 주소 & Null 포인터 Sentinel 스택의 오른쪽에서 왼쪽 순서로 Push ⇒ argv 요소
    1. argv[argc] 가 Null 포인터
    2. argv[0]이 가장 낮은 가상 주소에 위치하도록 보장
    3. 첫번째 푸시 전 스택 포인터를 8의 배수로 내림 처리
  4. %rsi → argv(argv[0]), %rdi → argc 설정
  5. 가짜 반환 주소 푸시

위와 같은 순서가 GitBook에 잘 작성되어 있었다.

strtok_r() 함수

입력받은 문자열을 delimiters 기준으로 구분한다.
Step 1인, 명령어를 단어들로 나누는 단계에서 사용할 수 있다.

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);

	/* Skip any DELIMITERS at our current position. */
	while (strchr (delimiters, *s) != NULL) {
		/* strchr() will always return nonnull if we're searching
		   for a null byte, because every string contains a null
		   byte (at the end). */
		if (*s == '\0') {
			*save_ptr = s;
			return NULL;
		}

		s++;
	}

스택에는 결국 어떤 값이 들어가나?

깃북에 있는 딱 이그림대로 들어간다.
유의해야할 것은 중간에 word-align이라는 이름 갖는 Padding 과
argv[4] 에 들어가는 NULL 값이다.

특히 Padding 같은 경우 8배수로 맞추는 역할을 하기 때문에, 여기서 삐끗하면 스택이 터진다.

1. 명령어를 단어들로 나누기

strtok_r 의 주석을 참고하여 명령어를 공백 기준으로 분리한다.

// 1.Break the command
	char *token;
	char *save_ptr;

	// file name에서 공백을 만나면 문자열 자르고 save_ptr에 다음 문자열 주소 저장
	// 첫번째 호출
	token = strtok_r(file_name, " ", &save_ptr); // args-single onearg 일경우 args-single

	char *argv[99];
	int argc = 0;

	while (token != NULL)
	{
		argv[argc] = token; // 자른 문자열 저장
		argc++;
		token = strtok_r(NULL, " ", &save_ptr); // args-single onearg 일경우 args-single
	}

	/* And then load the binary */
	success = load(file_name, &_if); // setup_stack 을 통해 rsp를 초기화 => argv 쌓기

2. 스택에 명령어 인자 Push & Padding 과 NULL 삽입

Padding 같은 경우 나머지 연산자를 활용, 8의 배수가 될때까지 0을 집어넣고
Top of Stack을 가리키는 포인터인 rsp를 그만큼 이동 시켰다.

// 2. Place the words at the top of Stack
	// first push 전 8의 배수로 round 후 push => 더 좋은 성능보장 목적
	// argv의 마지먁 idx부터 들어가야함.

	char *arg_stack_addrs[argc]; // 문자열의 스택 주소를 저장하기 위함

	for (int i = 0; i < argc; i++)
	{
		uint16_t length = strlen(argv[i]);
		_if.rsp = _if.rsp - (length + 1);						 // str의 길이 + \0 만큼 stack top pointer 이동(높은 주소 -> 낮은 주소)
		arg_stack_addrs[i] = _if.rsp;								 // TOS 주소 저장
		memcpy(arg_stack_addrs[i], argv[i], length); // 주소에 argv 값 복사해서 저장(Stack에 push)
	}

	// Padding(8의 배수로 round up)
	uintptr_t addrval = _if.rsp; // 현재 rsp (TOS) 주소값
	// 8로 나눈 나머지가 0으로 채울 공간
	while (_if.rsp % 8 != 0)
	{
		_if.rsp--; // 낮은 주소로 1바이트씩 이동
		*((char *)_if.rsp) = 0;
	}

	// NULL 삽입
	_if.rsp -= sizeof(char *); // char * 사이즈 만큼 이동
	*((char **)_if.rsp) = 0;	 // NULL 로 채우기

3. 각각의 문자열의 주소값과 null Pointer 값 넣기

// 3. Push the address of each string + null pointer(\0)
	for (int i = argc - 1; i >= 0; i--)
	{
		_if.rsp -= sizeof(char *);
		*((char **)_if.rsp) = arg_stack_addrs[i];
	}

4. %rsi가 argv(argv[0]) 가리키고, %rdi가 argc 가리키도록

_if.R.rdi = argc;
_if.R.rsi = arg_stack_addrs;

5. 마지막, 가짜 리턴 주소 푸시

// 5. 가짜 return address push

	_if.rsp -= sizeof(void *);
	*((void **)_if.rsp) = 0;

이제 hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true); 를 넣고
테스트 코드를 돌려서 확인 해보면

아래와 같이 나오면 성공!
단, 실제 테스트를 돌릴때는 저기 hex_dump를 빼주어야 PASS가 되니 유의 하자.

Trouble Shoot

Situation

args-single 테스트를 돌리는데 FAIL 과함께 Kernel Panic 이 와서 Call Stack을 찍어보았다.

Keyjungle@6e70038fa8f8:/workspaces/pintos_lab_docker/pintos-kaist/userprog/build$ backtrace 0x8004217d6c 0x800421c563 0x421c6e2 0x8004208cae 0x80042090cc 0x800421b3a4 0x80042076fc
0x0000008004217d6c: debug_panic (lib/kernel/debug.c:32)
0x000000800421c563: kill (userprog/exception.c:103)
0x000000800421c6e2: page_fault (userprog/exception.c:159 (discriminator 12))
0x0000008004208cae: intr_handler (threads/interrupt.c:352)
0x00000080042090cc: intr_entry (threads/intr-stubs.o:?)
0x000000800421b3a4: initd (userprog/process.c:73)
0x00000080042076fc: kernel_thread (threads/thread.c:559)

Find Cause(GDB 활용법 at PintOS)

원인을 찾기 위해 gdb 디버거를 활용하였다.
PintOS에서 GDB를 활용하는 법은 간단하다.

  1. make를 통해 build 파일이 존재한 상태를 전제로 한다.

  2. 터미널에 gdb 옵션을 달아서 테스트 코드를 실행한다.

pintos --gdb -m 20 --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg'
  1. 다른 터미널을 열고 gdb.kernel.o를 입력
  1. gdb 디버거가 실행중인 터미널(위 사진에서의 우측)에 target remote localhost:1234를 입력

아래와 같이 나오면 gdb 디버깅 준비가 모두 완료된다.

이후 break process_exec 을 통해 중단점을 잡고 n을 누르면서 한줄 씩 실행해보았다.

(gdb) n
196             while (token != NULL)
(gdb) n
198                     argv[argc] = token; // 자른 문자열 저장
(gdb) n
199                     argc++;
(gdb) n
200                     token = strtok_r(file_name, " ", &save_ptr); // args-single onearg 일경우 args-single
(gdb) n
196             while (token != NULL)
(gdb) n
198                     argv[argc] = token; // 자른 문자열 저장
(gdb) p argc
$14 = 60

내가 넘겨준건 args-single onearg 였다.
따라서 arg의 개수를 담는 변수인 argc가 2가 되어야 맞는데
60까지 더해지는, 즉 무한 루프를 돌고 있음을 발견 했다.

원인을 보니 args-single 로 잘랐는데 이걸 갱신한다는게 계속해서 넘겨주어서
무한 루프를 돌면서 계속 argc가 1씩 증가했던 것....

solution

	while (token != NULL)
	{
		argv[argc] = token; // 자른 문자열 저장
		argc++;
		token = strtok_r(NULL, " ", &save_ptr); // args-single onearg 일경우 args-single
	}

넘겨주는 인자를 수정했고, 에러를 해결할 수 있었다.

Conclusion

우리 모두 GDB를 애용합시다

profile
제 Velog에 오신 모든 분들이 작더라도 인사이트를 얻어가셨으면 좋겠습니다 :)

0개의 댓글