jun-wiki

View My GitHub Profile

Posts (Latest 10 updated) :
Read all
Contents:
  1. Argument Passing
    1. x86-64 호출 규약 (Calling Convention)
    2. 프로그램 시작 세부사항 (Program Startup Details)
      1. 예시
    3. 인자 전달 구현
    4. 추가 표
      1. 1. 문자열 영역과 포인터 테이블의 분리
      2. 2. 정렬(Alignment)
      3. 3. argv[argc] == NULL 센티널
      4. 4. 포인터 테이블의 주소 관계(푸시 순서의 결과)
      5. 5. 가짜 반환 주소와 초기 RSP
      6. 6. 레지스터 설정(호출 규약 일치)

지금 할 건 프로젝트 2 들어와서 가장 먼저 보이는거다

그건 바로…

인자 전달!
q(≧▽≦q)
꺄아아~~ (비명 지르는 중)


참고로 현재 독서왕 김독서 상태다

Argument Passing

인자 전달이라는 뜻

일단, process_exec()에서 사용자 프로그램의 인자 설정하기라는데

process_exec()가 뭔지부터 보자

유닉스의 execve()와 같은 역할을 하는 함수로

실행 중 프로세스의 껍데기(스레드/PID)는 냅두고
그 안의 사용자 프로그램을 새로 갈아 끼우는 역할을 한다

process_execute와 비슷하게 생겼지만 얘는 프로세스 새로 만드는 거고
process_exec는 기존 프로세스에 프로그램 이미지(주소공간)만 갈아끼워 쓰는 거다


x86-64 호출 규약 (Calling Convention)

x86-64의 호출 규약의 핵심 나온다

호출 규약이 뭐냐?

함수 호출을 위한 약속이다 뭐 전달하고 어떻게 반환값 받는지 등등

  • 사용자 수준 애플리케이션은 정수 인자 전달에 레지스터 순서 사용
    %rdi 1번째 인자
    %rsi 2번째 인자
    %rdx 3번째 인자
    %rcx 4번째 인자
    %r8   5번째 인자
    %r9   6번째 인자

  • 호출자는 자신의 다음 명령의 주소를 스택에 푸쉬하고
    피호출자의 첫 명령으로 점프 CALL로 이 두가지 모두 수행

  • 피호출자가 실행된다

  • 피호출자가 반환값이 있다면 이를 레지스터 RAX에 저장한다

  • 피호출자는 스택에서 반환 주소 팝해 해당 위치로 점프한다
    이것이 RET 명령어

실제 동작시에는 추가 동작이 들어가기에 RET이 더 안전하고 빠르게 복귀하게 해준다


프로그램 시작 세부사항 (Program Startup Details)

Pintos의 사용자 프로그램용 C 라이브러리는 lib/user/entr.c_start()를 시작점으로 삼는다
(프로그램의 엔트리 포인트)

main() 호출하고 반환되면 exit() 호출한다

void
_start (int argc, char *argv[]) {
    exit (main (argc, argv));
}


커널은 시작전 초기 함수의 인자들을 레지스터에 적재해야 한다

인자 전달 방식은 호출 규약과 동일

예시

/bin/ls -l foo bar라는 명령의 인자 처리 시

  1. 명령을 단어(토큰)로 분할: /bin/ls, -l, foo, bar

  2. 이 문자열들을 스택 상단에 배치
    어차피 포인터로 참조해 배치 순서는 상관 ✕

  3. 각 문자열의 주소 뒤에 널 포인터 센티널 \0 을 더해 끝을 표시하고,
    오른쪽에서 왼쪽 순서로 스택에 푸시

    이것들이 argv의 요소

    • 널 포인터 센티널은 argv[argc]가 널포인터가 되도록 보장
      (C표준 요구 사항, 문자열 마지막 규칙)
    • 이 순서는 argv[0]가장 낮은 가상주소에 오도록 한다
    • 워드 정렬(정렬된 접근, word-aligned)이 비정렬 접근보다 빠르므로,
      첫 푸시 전에 스택 포인터를 8의 배수로 내림(round down) 처리하면 성능 향상

가장 낮은 가상 주소는 스택의 꼭대기다

  1. %rsiargv(= argv[0]의 주소)를 가리키게 하고, %rdi에는 argc를 설정

  2. 마지막으로, 가짜 “반환 주소”를 하나 푸시
    실제 반환하지 않지만, 호출 규약 맞추며,
    만약에 RET에 의해 복귀해도 반환주소가 없어 페이지 폴트
    -> 깨끗이 종료 처리
    스택 프레임 모양도 맞추었기에 다루기도 편하다


인자 전달 구현

현재 process_exec()은 새 프로세스에 인자를 전달하지 않으니 이를 구현하면 된다

단순 파일 이름 받는 대신 공백 기준 단어로 분해하며

첫 단어는 프로그램 이름, 두 번째 단어는 첫 인자 등등으로 처리

예) process_exec("grep foo bar")grep을 실행하며 인자로 foobar를 전달


인자 문자열 파싱 방식은 자유
include/lib/string.h에 프로토타입 존재
lib/string.c에 주석이 풍부하게 구현된 strtok_r()도 참고 ㄱㄴ

부족하다면 터미널에서 man strtok_r로 매뉴얼 페이지를 확인


추가 표

안 넣을라고 했는데 넣겠다

주소 이름 데이터 타입
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 (*)()

1. 문자열 영역과 포인터 테이블의 분리

  • 위쪽(더 높은 주소)에 '/bin/ls\0', '-l\0', 'foo\0', 'bar\0' 같은 실제 문자열 바이트가 쭉 놓여 있다
  • 아래쪽(더 낮은 주소)에 그 문자열들을 가리키는 포인터 배열(argv) 이 놓여 있다

    • 표에서 argv[3][...](문자열) vs argv[3](포인터) 표기가 다른 이유
    • argv[i]문자열의 주소(포인터) 고, argv[i][...]그 주소가 가리키는 실제 문자열 바이트

2. 정렬(Alignment)

  • 문자열과 포인터 배열 사이에 word-align으로 0 채움이 있어요(8의 배수 정렬)
  • 문자열은 아무 순서로 놔도 포인터로 참조하니 상관없다만, 푸시 시작 전 스택 포인터를 8바이트 정렬로 맞춰라는 규칙을 구현했다는 걸 보여준다

3. argv[argc] == NULL 센티널

  • 포인터 테이블의 마지막 원소(argv[4])가 0(NULL)로 세팅되어 있다
    → C·POSIX 규약대로, 인자 배열이 NULL로 끝나는 형식을 만족

4. 포인터 테이블의 주소 관계(푸시 순서의 결과)

  • 포인터들은 오른쪽→왼쪽(뒤에서 앞으로) 푸시되므로, 주소가 아래쪽으로 갈수록(낮을수록) argv 인덱스가 작아진다

    • 그래서 argv[0](프로그램명 포인터)가 테이블의 가장 낮은 주소(0x4747ffc0)에 놓이고,
    • 그 위로 argv[1], argv[2], argv[3], 그리고 맨 위가 argv[4]=NULL 이 된다

5. 가짜 반환 주소와 초기 RSP

  • 가장 아래(더 낮은 주소)에 return address = 0이 푸시되어 있고, 초기 스택 포인터(RSP)는 그 자리(0x4747ffb8) 를 가리킨다

6. 레지스터 설정(호출 규약 일치)

  • RDI = argc (= 4), RSI = &argv[0] (= 0x4747ffc0)
    → x86-64 System V 규약에 맞춰 main(int argc, char *argv[])에 들어갈 값이 준비된 상태임을 확인시켜준다