x86-64에서 프로시저에서 프로시저로의 데이터 전달
- 레지스터를 통해서 일어난다. ( 인자들이 %rdi, %rsi등으로 전달되고 %rax로써 리턴되는 형태들을 의미 )
- 프로시저 P가 프로시저 Q를 호출할 때, P는 자신이 전달할 인자들을 레지스터에 복사해야 한다.
- 전달할 인자들이 레지스터에 복사되거나 스택에 할당된 후에!!!!!! Q를 호출하게 된다.
- Q가 일을 끝내고 P로 리턴할 때, P는 Q가 리턴한 값을 %rax에서 접근할 수 있다.
x86-64에서 프로시저에서 프로시저로의 데이터 전달2
- x86-64에서는 “최대 여섯 개의 정수형 인자(정수와 포인터)”가 레지스터로 전달될 수 있다.
- 레지스터는 “전달되는 데이터 형의 길이”에 따라서, 정해진 순서대로 이용된다.
- “프로시저의 인자 전달(송신) 기능”을 수행할 수 있는 레지스터는, %rdi, %rsi, %rdx, %rcx, %r8, %r9 “오직 64비트 레지스터 6개” 뿐이다.
- 만약 32비트, 16비트같은 작은 데이터를 전달할 것이라면 64비트 레지스터의 하위 비트를 이용해서 전달하며, 이 데이터를 “받을 때”는 32비트, 16비트 (%eax, %di같은) 레지스터를 이용하여 받을 수 있다.
- 인자 전달에 사용되는 6개의 64비트 레지스터 이외의 레지스터들은, 각각 상황에 따라서 다양하게 사용될 수 있다.
x86-64에서 프로시저에서 프로시저로의 데이터 전달3
- 여섯 개보다 많은 인자를 전달할 때, 6개를 초과한 인자들은 “스택”으로 전달된다.
- 함수 P가 여섯 개 초과의 인자를 전달하려 할 때, 인자들은 스택에 먼저 push된다. 인자들이 모두 push되고 나면, P는 Q를 call 할 수 있다. call을 하면 return address가 스택에 push된다.
- 이후 Q가 활동하기 시작하면 , callee-saved 레지스터의 값들을 스택에 push한다. (물론 안 할 수도 있다).
즉 스택 ==>> [Bottom] 전달할인자 / return address / calleesaved값들 [Top] 형태이다.
x86-64에서 프로시저에서 프로시저로의 데이터 전달4
- 인자들이 스택으로 전달될 때, 모든 데이터의 길이는 8바이트 단위로 맞춰진다.
- 인자들이 스택에 배치되고 난 후, 다음 함수를(Q) 호출할 call 인스트럭션을 실행할 수 있다.
- 호출된 프로시저 Q는 레지스터와 스택을 통해 전달받은 인자들에 접근할 수 있다.
- movq 16(%rsp), %rax /// movl 8(%rsp), %edx 이런 식으로 스택의 리턴주소(top) 앞쪽(Bottom쪽)에 있는 인자들에 접근하여 값을 가져온다.
오퍼랜드의 데이터 길이에 따라서 addq, addl등 인스트럭션 끝 문자가 바뀐다.
- addq, addl, addw, addb 각각 8, 4, 2, 1 바이트이다.
- 또한 movl 8(%rsp), %edx 이렇게 데이터를 받아왔어도, 다음 명령줄에서 addb %dl, (%rax) 이렇게 하위 1바이트만 가져와 활용할 수도 있다. (근데 어떻게 되는 거지?)
연습문제 3.3
이미지가 끊겼는데.. 어쨌든 한 문제다.
문제 : procprob 함수의 4개 인자인 u, a, v, b의 자료형을 알아내기
답 :
- 3번, 4번 줄이 위 코드에 해당하는 *u += a; 와 *v += b 라고 가정한다.
먼저 일어나는 연산은 *u += a; 즉 addq %rdi, (%rdx) 이므로, (%rdx)는 *u를 의미하고 %rdi는 a를 의미한다는 걸 알 수 있다. 하지만 2번 줄에서 %edi가 %rdi로 복사되고 있으므로, a는 원래 %edi에 있었다가 %rdi로 옮겨지며 길이가 변환되었다는 걸 알 수 있다. 길이가 변환된 이유는 destination 오퍼랜드인 (%rdx)에 길이를 맞춰야 하기 때문이다.
그리고 덧셈 연산을 수행하려면 정수형이어야 하므로 “a는 int, *u는 *long”이다.
- addb %sil, (%rcx) 에서의 destination 오퍼랜드는 *v += b; 의 *v 이므로, (%rcx)는 *v이고 %sil이 b 라는 걸 알 수 있다. %sil은 1byte 크기이기 때문에 destination인 *v는 1byte 크기인 *char 라는 걸 알 수 있다. 여기서 마찬가지로 b도 1byte인 char 라고 생각할 수도 있겠지만, 5번, 6번 줄을 보면 상수 6을 리턴하고 있다. 즉 sizeof(a) + sizaof(b) 값이 6 이라는 것이다. a는 int형이므로 4바이트이니, b는 원래 2바이트였다는 걸 유추할 수 있다. 즉 “b 는 short, *v는 *char" 이다.
프로시저의 인자가 스택에 저장되는 경우
- 레지스터의 수가 부족할 경우( 7개 이상일 경우 )
- 지역변수에 ‘&’연산자가 사용되어서, 이 변수의 주소가 생성되어야 할 때
- 배열/구조체일 경우
- 지역 저장소(스택)가 요구될 때(&연산자 등) %rsp를 늘려 스택 프레임을 할당하고, 함수가 완료되었을 때 이를 반환한다.
프로시저와 레지스터에 들어가며..
- 레지스터는 모든 프로시저들이 공유하는 단일 자원의 역할을 한다.
- 프로시저가 다른 프로시저를 호출할 때, 피호출자는 호출자가 나중에 사용할 계획인 레지스터의 값을 변경하지 않는다. 또는, 변경하되 기존의 값이 손실되지 않도록 스택에 push해둔다.
- 이처럼 x86-64는 모든 프로시저들이 준수해야 할 ”통일된 레지스터 사용규칙“을 채택했다.
callee-saved 레지스터(피호출자 - 저장 레지스터)
- %rbx, %rbp, %r12 ~ %r15 총 6개 레지스터가 해당된다.
- 프로시저 P가 프로시저 Q를 호출했다면, Q가 리턴될 때의 레지스터 값이 호출 시의 레지스터 값과 동일하도록 해야 한다.
- callee-saved 레지스터에 있는 값들은 피호출자에 들렀다가 리턴되어 호출자로 돌아와도 값이 유지된다.
- callee-saved 레지스터에 있는 값들을 스택에 push해두고, 리턴 직전에 pop해와서 값을 보존하고 리턴하는 형식이다. ( push해둔 후 피호출자는 해당 레지스터들을 연산에 이용할 수 있다 )
- 이렇게 push된 값들은 return address의 이후(Top쪽)에 존재한다.
스택 포인터(%rsp)의 특이한 점
- %rsp는 공식적으론 callee-saved 레지스터로 분류되지 않지만, 피호출자가 리턴되고 호출자로 돌아왔을 때 값이 항상 동일하기 때문에 callee-saved의 기능을 수행한다고도 볼 수 있다.
caller-saved 레지스터(호출자-저장 레지스터)
- callee-saved, 6개의 argument, Return value(%rax), Stack Pointer(%rsp) 이렇게 14개 레지스터를 제외하고 남은 나머지 2개가 caller-saved 레지스터이다.
- caller-saved 레지스터 : %r10, %r11
- caller-saved 레지스터에 있는 값들은 함수에 의해 변경될 수 있다.
- 따라서, 호출자는 피호출자를 호출하기 전에 데이터를 저장해 놓아야 한다.
이제는 이 그림을 이해할 수 있다.
%rax : 리턴 value가 저장되는 레지스터
%rbx, %rbp, %r12 ~ %r15 : collee-saved 레지스터, 프로시저를 호출한 후 리턴되어 돌아와도 이 레지스터의 값들은 변하지 않는다.
%rsp : 스택 포인터. 항상 스택의 top을 가리킨다.
%rdi, %rsi, %rdx, %rcx, %r8, %r9 : argument, 즉 프로시저 호출 시 인자를 전달(송신)할 때 사용하는 레지스터들
%r10, %r11 : caller-saved 레지스터, 이 레지스터들은 함수 호출 시 값이 변할 수 있다.
재귀 프로시저
- 레지스터와 스택의 사용법 만으로 재귀 호출을 설명할 수 있다.
- 각 프로시저 콜은 메모리에 개별 공간을 가지고, 따라서 여러 프로시저들의 지역변수들은 서로 간섭하지 않는다.
- 스택 운영 방식은 프로시저가 호출될 때 스택을 할당하고 리턴할 때 반환하는 방식이다.
- 재귀 프로시저도 일반적인 프로시저의 호출과 크게 다르지 않다.
추가로 배운 인스트럭션들
jle(Jump if Less than or Equal)
- 주로 부호 있는 정수 비교에서 사용됨
- 비교 결과가 작거나 같을 경우 지정된 레이블로 분기(이동)한다
- jle .L35 -> 비교 연산의 결과가 같거나 작으면 .L35레이블로 이동한다
je
- ZF플래그가 설정되어 있으면 분기(이동)한다.
cmp(compare, 비교)
- 뺄셈을 이용해서 두 개의 피연산자를 비교한다 ( destination에서 source를 뺀다 )
- 뺀 결과값이 저장되는 건 아니고, ”플래그 레지스터“의 상태를 변경한다.
- jle 같은 점프 인스트럭션의 다음 행동은, 플래그 레지스터의 값에 따라 결정된다.
플래그 레지스터(Flag Register)
- 설정된다 = 값을 1로 설정한다
- Zero Flag(ZF) : 연산 결과가 0인 경우 설정된다. 즉 cmp 인스트럭션에서 두 값이 같을 경우를 의미
- Sign Flag(SF) : 연산 결과가 음수인 경우 설정된다.
- Overflow Flag(OF) : 연산 결과가 정수 오버플로우가 발생한 경우 설정
- Carry Flag(CF) : 덧셈 연산 최상위 비트에서 올림 발생 / 뺄셈 연산에서 빌림 발생한 경우 설정
- Parity Flag, Auxiliary Carry Flag 이런것도 있는데 잘 쓰이지 않는다.
test
- 논리 AND연산 수행, 일반적으로 점프 인스트럭션과 함께 사용된다.
- testl %eax, %eax -> %eax가 0인지 아닌지 확인 가능하다.
- ex) 0 AND 0 = 0 // 1 AND 1 = 1
- %eax가 0이면 결과는 0이므로, ZF플래그가 1로 설정된다. 이것은 다음 줄의 je와 연결된다.
- je 레이블명 -> ZF 플래그가 1이면 해당 레이블로 점프한다.
- 즉 eax가 0이면 이동하도록 하는 식으로 사용될 수 있다.
shr
- shrl $1, %eax -> %eax 레지스터의 값을 1비트 오른쪽 시프트 연산한다
함수가 실행될 때, add나 mov등 인스트럭션들이 실행된 결과는 %rax 레지스터에 계속해서 갱신된다.
하지만 pop 인스트럭션의 결과는 %rax에 저장되지 않는다!!!!
연습문제 3.35
A. rfun 함수는 callee-saved 레지스터인 %rbx에 무슨 값을 저장하는가?
답 : 입력값 x
B. 위 C 코드에 빠진 코드를 채우시오.
답 :
if x = 0 ==> testq %rdi, %rdi 그리고 je .L2 부분을 보면, rdi가 0일 경우 점프하도록 코드가 짜여진 것을 볼 수 있다. 그에 대한 자세한 설명은 위 test 인스트럭션 설명에서 했다.
return 0; ==> L2레이블로 점프하여 popq를 만나 스택에 push했던 %rbx를 복구시키고, 바로 ret를 통해 return 하고있다.
여기서 왜 0을 return 하는지 헷갈릴 수 있는데, 어셈블리어의 진행에서는 mov, add 등의 인스트럭션들의 결과를 항상 %rax에 갱신한다. 어셈블리 코드 4행을 보면 movl $0, %eax 부분이 있는데 이 때 0 이라는 값이 %rax에 갱신된다. %rax는 return value를 저장하는 레지스터이기 때문에 return 0을 하는 것이다.
unsigned long nx = x >> 2; ==> 여기가 제일 쉬울 것 같다. 어셈블리 코드 7행의 shrq $2, %rdi는 %rdi에 대해 오른쪽으로 2번 시프트 연산을 하라는 뜻이다. %rdi엔 x가 저장되어 있으니 답은 x >> 2; 이다.
return x + rv; ==> C 코드를 보면 long rv = rfun(nx); 이런 식으로 rfun(nx)의 리턴값을 rv 변수에 저장하고 있다. 동시에 어셈블리 8행에서는 리턴값이 %rax에 저장된다. 이후 9행에서 addq %rbx, %rax 를 하고 있는데 %rbx에는 x가 저장되어 있다. 즉 rv = rv + x인 것이다. 이 부분은 x + rv를 써도 되고 rv + x 를 써도 된다.
또한 어셈블리 코드에 ret 인스트럭션이 한개밖에 없어서 점프문을 거치지 않으면 어떻게 리턴을 하라는 거냐 라고 생각할 수 있다. 하지만 어셈블리 코드는 je 인스트럭션에 의해 점프하지 못했더라도, 어차피 명령행이 한 줄씩 실행되며 L2 레이블까지 실행하게 된다. 따라서 ret에 도달할 수 있다.
'크래프톤 정글 > TIL' 카테고리의 다른 글
[크래프톤 정글 5기] week04 C언어 주차 여섯번째 날, 이진 검색 트리, B-Tree (0) | 2024.04.16 |
---|---|
[크래프톤 정글 5기] week04 C언어 주차 두번째 날, C언어 문법, 포인터 (0) | 2024.04.12 |
[크래프톤 정글 5기] week03 알고리즘 주차 스무번째 날, 프로시저, 리턴주소, 함수호출, 디스어셈블 코드 실행추적 (1) | 2024.04.09 |
[크래프톤 정글 5기] 스택, 레지스터, 꼬리 재귀 최적화 (0) | 2024.04.09 |
[크래프톤 정글 5기] 플로이드 워셜 알고리즘 + 파이썬 구현 (0) | 2024.04.08 |