지금 할 건 프로젝트 2 들어와서 가장 먼저 보이는거다
그건 바로…
인자 전달!
q(≧▽≦q)
꺄아아~~ (비명 지르는 중)
참고로 현재 독서왕 김독서 상태다
인자 전달이라는 뜻
일단, process_exec()
에서 사용자 프로그램의 인자 설정하기라는데
process_exec()
가 뭔지부터 보자
유닉스의 execve()
와 같은 역할을 하는 함수로
실행 중 프로세스의 껍데기(스레드/PID)는 냅두고
그 안의 사용자 프로그램을 새로 갈아 끼우는 역할을 한다
process_execute
와 비슷하게 생겼지만 얘는 프로세스 새로 만드는 거고
process_exec
는 기존 프로세스에 프로그램 이미지(주소공간)만 갈아끼워 쓰는 거다
x86-64의 호출 규약의 핵심 나온다
호출 규약이 뭐냐?
함수 호출을 위한 약속이다 뭐 전달하고 어떻게 반환값 받는지 등등
사용자 수준 애플리케이션은 정수 인자 전달에 레지스터 순서 사용
%rdi
1번째 인자
%rsi
2번째 인자
%rdx
3번째 인자
%rcx
4번째 인자
%r8
5번째 인자
%r9
6번째 인자
호출자는 자신의 다음 명령의 주소를 스택에 푸쉬하고
피호출자의 첫 명령으로 점프 CALL
로 이 두가지 모두 수행
피호출자가 실행된다
피호출자가 반환값이 있다면 이를 레지스터 RAX
에 저장한다
피호출자는 스택에서 반환 주소 팝해 해당 위치로 점프한다
이것이 RET
명령어
실제 동작시에는 추가 동작이 들어가기에
RET
이 더 안전하고 빠르게 복귀하게 해준다
Pintos의 사용자 프로그램용 C 라이브러리는 lib/user/entr.c
의 _start()
를 시작점으로 삼는다
(프로그램의 엔트리 포인트)
main()
호출하고 반환되면exit()
호출한다void _start (int argc, char *argv[]) { exit (main (argc, argv)); }
커널은 시작전 초기 함수의 인자들을 레지스터에 적재해야 한다
인자 전달 방식은 호출 규약과 동일
/bin/ls -l foo bar
라는 명령의 인자 처리 시
명령을 단어(토큰)로 분할: /bin/ls
, -l
, foo
, bar
이 문자열들을 스택 상단에 배치
어차피 포인터로 참조해 배치 순서는 상관 ✕
각 문자열의 주소 뒤에 널 포인터 센티널 \0
을 더해 끝을 표시하고,
오른쪽에서 왼쪽 순서로 스택에 푸시
이것들이 argv
의 요소
argv[argc]
가 널포인터가 되도록 보장 argv[0]
가 가장 낮은 가상주소에 오도록 한다가장 낮은 가상 주소는 스택의 꼭대기다
%rsi
는 argv
(= argv[0]
의 주소)를 가리키게 하고, %rdi
에는 argc
를 설정
마지막으로, 가짜 “반환 주소”를 하나 푸시
실제 반환하지 않지만, 호출 규약 맞추며,
만약에 RET
에 의해 복귀해도 반환주소가 없어 페이지 폴트
-> 깨끗이 종료 처리
스택 프레임 모양도 맞추었기에 다루기도 편하다
현재 process_exec()
은 새 프로세스에 인자를 전달하지 않으니 이를 구현하면 된다
단순 파일 이름 받는 대신 공백 기준 단어로 분해하며
첫 단어는 프로그램 이름, 두 번째 단어는 첫 인자 등등으로 처리
예)
process_exec("grep foo bar")
는grep
을 실행하며 인자로foo
와bar
를 전달
인자 문자열 파싱 방식은 자유
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 (*)() |
'/bin/ls\0'
, '-l\0'
, 'foo\0'
, 'bar\0'
같은 실제 문자열 바이트가 쭉 놓여 있다아래쪽(더 낮은 주소)에 그 문자열들을 가리키는 포인터 배열(argv
) 이 놓여 있다
argv[3][...]
(문자열) vs argv[3]
(포인터) 표기가 다른 이유argv[i]
는 문자열의 주소(포인터) 고, argv[i][...]
는 그 주소가 가리키는 실제 문자열 바이트word-align
으로 0 채움이 있어요(8의 배수 정렬)argv[argc] == NULL
센티널argv[4]
)가 0(NULL)로 세팅되어 있다포인터들은 오른쪽→왼쪽(뒤에서 앞으로) 푸시되므로, 주소가 아래쪽으로 갈수록(낮을수록) argv 인덱스가 작아진다
argv[0]
(프로그램명 포인터)가 테이블의 가장 낮은 주소(0x4747ffc0
)에 놓이고,argv[1]
, argv[2]
, argv[3]
, 그리고 맨 위가 argv[4]=NULL
이 된다return address = 0
이 푸시되어 있고, 초기 스택 포인터(RSP
)는 그 자리(0x4747ffb8
) 를 가리킨다RDI = argc (= 4)
, RSI = &argv[0] (= 0x4747ffc0)
main(int argc, char *argv[])
에 들어갈 값이 준비된 상태임을 확인시켜준다