- Normal (형태1) : R -> Mem[Reg[R]]
해석 ) 레지스터 R 이 가리키는 메모리에 저장된 값 ( Mem[Reg[R]] ) 을 또 다른 레지스터 ( R ) 에 값을 복사하는 것
- 예시) movq (%rcx), %rax
- rcx 레지스터에 저장된 값을 rax 레지스터에 값을 복사
- Displacement (형태2) : Mem[Reg[R] + D]
해석 ) 레지스터 R 이 가리키는 메모리에서 D 바이트 만큼 떨어진 곳에 위치한 메모리에 access
- 예시) movq 8(%rbp), %rdx
- 레지스터 rbp 의 메모리 주소에서 8 만큼 더 주소를 이동해서 그곳에 있는 메모리에 저장된 값을 rdx 레지스터에 복사
주석처리 # 가 되어있는 부분이 c언어 코드로, 이게 어셈블리 코드로 바뀐 것이다!
movq (%rai), %rax : rai 레지스터 포인터가 가리키는 메모리에 접근해서 얻어온 값을 rax 가 가리키는 메모리에 값을 할당
movq (%rsi), %rdx : 위 코드와 동일한 원리
movq %rdx, (%rdi) : 동일한 원리
movq %rax, (%rsi) : 동일한 원리
=> 연산 결과, rdi 와 rsi 가 가리키는 각 메모리의 값이 swap 된다!
형태 : D(Rb, Ri, S) -> Mem[Reg[Rb] + S*Reg[Ri] + D]
- D : Displacement (일정 배수. 1,2,4 byte 중 하나)
Rb : Base Register (%rax를 포함한 15개의 register 중 하나. 메모리의 base 주소를 알려주는 용도)
Ri : Index Register (인덱스. %rsp 를 제외한 모든 레지스터)
S : Scale, Size 를 의미 (1,2,4,8 등의 상수) => int형 배열인 경우 다음 메모리 이동시 4바이트 만큼 뛰는데, 이때 4바이트의 "4" 가 Scale 상수이다.
=> 간단 해석 : 특정 메모리를 access 하고싶은경우, 그 특정 메모리 주소값 계산법이 바로 위와 같은 계산 식 인것이다.
- 계산식 정리 : access 하고 싶은 특징 메모리의 주소값 = "base registser 의 값 + (Scale 크기 x index register 의 값) + Displacement 값" 이 되는 것이다.
rdx, rcx 라는 레지스터가 있고 각각 위와 같은 메모리 주소를 가지고 있고, 새로운 메모리에 접근하는 4가지 예제가 있다.
2) ( %rdx, %rcx ) : rdx, rcx 각각이 위 식에서 Rb, Ri 가 된다. 그리고 D와 S의 값은 없다.
=> 둘을 그냥 더하면 된다!
3) (%rdx, %rcx, 4) : S = 4 이다. rdx 레지스터가 가리키는 메모리 주소값 + (4 x rcx 레지스터가 가리키는 메모리 주소값)
4) Ox80( , %rdx, 2) : Ri 가 없다. rdx 레지스터가 가리키는 메모리 주소값에다 2를 곱한 값 + D (= Ox80) 를 더해주면 된다!
- 형태 : leaq src, dst
- movq 와의 공통점 : movq 와 연산식도 동일하고, movq 와 leaq 둘 다 주소를 계산하는 명령어이다.
- movq 와의 차이점 : movq 와 거의 비슷하지만, movq 와 달리 메모리를 access 하지 않고, 그냥 계산만 한다!
=> movq 는 계산식을 계산하여 얻어낸 메모리 주소값에 접근하여 메모리에 저장된 값을 가져온다.
반면 leaq 는 계산된 값을 취하기만 할 뿐 메모리에 접근하지는 않는다.
ex) movq(%rdi, %rdi, 2) : 이건 주소값 계산이지만,
leaq(%rdi, %rdi, 2) : rdi 레지스터에 저장된 값에 대한 계산이다.
=> 즉, movq 의 경우 rdi 레지스터의 주소값 + 2 x (rdi 주소값) = 3 x (rdi 주소값) 이라는 해당 주소값에 접근하는 것이다.
반면 leaq 의 경우는 rdi 레지스터에 저장된 값 + 2 x (rdi 레지스터에 저장된 값) = 3 x (rdi 레지스터에 저장된 값) 으로 주소값이 아닌 평범한 상수 값에 대한 계산이다!
예제
input 값에다 12 를 곱해주는 함수이며,
왼쪽 c언어 코드를 컴파일하면 오른쪽 어셈블리어로 바뀐다.
cf) c언어 코드와 왜 위와 같은 어셈블리 코드로 바뀌었는지 이해할 필요없다!
(너무 복잡하니, 어셈블리 코드만보고 어떤 코드인지만 이해해도 충분하다.)
1) leaq (%rdi, %rdi, 2), %rax # t <- x+x*2
=> rdi + (2 + rdi) 의 결과값은 3 x %rdi 가 나오는데 이를 rax 에 할당한 것이다.
2) salq $2, %rax # return t << 2
=> %rax 값에 대해 왼쪽 쉬프트 연산이 2번 일어났다. 따라서
%rax = 3 x %rdi 라 했으므로, (3 x %rdi) x 2^2 = 12 x %rdi 가 된것이다.
결국 input 값인 %rdi 에 대해 12가 곱해지는 연산이 이루어진 것이다!
주의) leaq (%rdi, %rdi, 2), %rax 에서 괄호 안의 연산 "(%rdi, %rdi, 2)" 은
메모리를 접근하는 과정이 발생하지 않았다! 그냥 단순히 값만 가지고 계산하는 것이다. (이것이 movq 와의 차이점)
16개의 레지스터가 존재 (%rax, %rbx, ..)
위 레지스터외에도 instrction pointer 와 condition code 라는 레지스터들이 또 있다.
=> CF : unsigned 숫자들을 64bit 짜리로 표현하는데 어떤 계산을 했을때의 결과값이 64bit 가 표현할 수 있는 범위를 벗어날 떄 (오버플로우가 발생할 때) 넘치는 65번째 1이 생기면 CF bit 가 올라간다.
=> SF : signed 숫자에 대해 64bit 시스템에서의 부호비트 (숫자가 0보다 작으면 부호비트가 1로바뀜)
=> ZF : 숫자가 0일때 비트가 1로바뀜
=> OF : 오버플로우가 발생할떄 1로바뀜
(=> b, a 둘다 source 이다. destination 이 없어서 결과값이 없는 것이다! )
CF : MSB(Most Significant Bit) 에서 오버플로우 발생시 1로바뀜
SF : a - b < 0 일때 1로 바뀜
ZF : a == b 일떄 1로 바뀜
OF : signed 2의보수 시스템에서 오버플로우 발생시 1로바뀜
ex) sete : condition 코드 ZF 가 1일떄 destination 으로 1을 설정한다.
rax 레지스터의 경우 destination 이 %al 이고, rsp 레지스터의 경우 destination 이 %spl 이다.
%al 은 output 으로 setg 를 포함함하는 LSB(Least Significant Byte) 이다.
C언어 함수 코드 gt(greater than) 을 보면 알수있듯이,
%rdi 와 %rsi 를 비교하고 그 결과값(0 또는 1을) %rax 에 넣으려하는 것이다.
c언어를 컴파일 하면 위와같은 4줄짜리 어셈블리 코드가 나온다.
c언어 gt 함수 코드가 어셈블리 코드로 변홛된 코드 해설
cmpq %rsi %rdi : %rsi 와 %rdi 에 대한 cmpq 명령어의 결과값을 (0 또는 1이 결과값 일것이다.)
setg %al : %al 에 결과값을 할당한다
movzbl %al, %eax : %al 를 제외한 나머지 byte 공간들을 모두 0으로 설정해주는 놈
(=> rax 레지스터에서 LSB 공간인 %al 을 제외한 나머지 byte 공간을 모두 0으로 설정)
ret
예제
c언어 코드 : absdiff(absolute difference) : 두 숫자의 절댓값 크기 차이를 리턴
jle .L4 => %rsi 가 %rdi 보다 작을경우 ".L4" 구문으로 점프뛴다.
반대로 작지 않다면 그냥 아래 코드들을 줄줄이 차례대로 수행한다.
conditional branch 에서는 왼쪽과 같은 코드를 자동으로 오른쪽처럼 변환시켜준다.
앞서 살핀 코드를 다시 살펴보면 아래와 같다.
이 conditional code 를 살펴보면 if문으로 검사를 하고 jump 를 할지말지 여부를 결정한다. 이런 방식은 jump 한다는 과정 자체가 굉장히 비효울적이다.
예제
어셈블리 코드 자세히 보기
( 몰론 대신에 %rsi - %rax 와 %rax - %rsi 값을 모두 계산해야 한다는 점이 있긴하다. 그래도 jump 과정이 생략되었다는 점에서 효율적이다! )
잘 봤습니다!