크래프톤 정글/TIL

[크래프톤 정글 5기] week03 알고리즘 주차 스물한번째 날, 프로시저, C / 어셈블리 코드 변환, callee-saved, 플래그 레지스터

양선규 2024. 4. 10. 21:01
728x90
반응형

함수 인자 전달에 관한 레지스터들

 

x86-64에서 프로시저에서 프로시저로의 데이터 전달

- 레지스터를 통해서 일어난다. ( 인자들이 %rdi, %rsi등으로 전달되고 %rax로써 리턴되는 형태들을 의미 )

- 프로시저 P가 프로시저 Q를 호출할 때, P는 자신이 전달할 인자들을 레지스터에 복사해야 한다.

- 전달할 인자들이 레지스터에 복사되거나 스택에 할당된 후에!!!!!!   Q를 호출하게 된다.

- Q가 일을 끝내고 P로 리턴할 때, PQ가 리턴한 값을 %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

연습문제 3.3

 

 

이미지가 끊겼는데.. 어쨌든 한 문제다.

문제 : procprob 함수의 4개 인자인 u, a, v, b의 자료형을 알아내기

 

:

- 3, 4번 줄이 위 코드에 해당하는 *u += a; *v += b 라고 가정한다.

먼저 일어나는 연산은 *u += a; addq %rdi, (%rdx) 이므로, (%rdx)*u를 의미하고 %rdia를 의미한다는 걸 알 수 있다. 하지만 2번 줄에서 %edi%rdi로 복사되고 있으므로, a는 원래 %edi에 있었다가 %rdi로 옮겨지며 길이가 변환되었다는 걸 알 수 있다. 길이가 변환된 이유는 destination 오퍼랜드인 (%rdx)에 길이를 맞춰야 하기 때문이다.

그리고 덧셈 연산을 수행하려면 정수형이어야 하므로 “aint, *u*long”이다.

 

- addb %sil, (%rcx) 에서의 destination 오퍼랜드는 *v += b; *v 이므로, (%rcx)*v이고 %silb 라는 걸 알 수 있다. %sil1byte 크기이기 때문에 destination*v1byte 크기인 *char 라는 걸 알 수 있다. 여기서 마찬가지로 b1bytechar 라고 생각할 수도 있겠지만, 5, 6번 줄을 보면 상수 6을 리턴하고 있다. sizeof(a) + sizaof(b) 값이 6 이라는 것이다. aint형이므로 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 -> %eax0인지 아닌지 확인 가능하다.

- ex) 0 AND 0 = 0 // 1 AND 1 = 1

- %eax0이면 결과는 0이므로, ZF플래그가 1로 설정된다. 이것은 다음 줄의 je와 연결된다.

- je 레이블명  -> ZF 플래그가 1이면 해당 레이블로 점프한다.

- eax0이면 이동하도록 하는 식으로 사용될 수 있다.

 

shr

- shrl $1, %eax -> %eax 레지스터의 값을 1비트 오른쪽 시프트 연산한다

 

함수가 실행될 때, addmov등 인스트럭션들이 실행된 결과는 %rax 레지스터에 계속해서 갱신된다.

하지만 pop 인스트럭션의 결과는 %rax에 저장되지 않는다!!!!

 

 

 

 

연습문제 3.35

연습문제 3.35

 

A. rfun 함수는 callee-saved 레지스터인 %rbx에 무슨 값을 저장하는가?

답 : 입력값 x

B. 위 C 코드에 빠진 코드를 채우시오.

답 :

B 정답

 

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에 도달할 수 있다.

728x90
반응형