어셈블리어의 Memory Addressing 구조

msung99·2022년 9월 26일
2
post-thumbnail

Simple Memory Addressing (간단한 메모리 접근시 주소계산 및 값 할당방법)

  • Normal (형태1) : R -> Mem[Reg[R]]
    해석 ) 레지스터 R 이 가리키는 메모리에 저장된 값 ( Mem[Reg[R]] ) 을 또 다른 레지스터 ( R ) 에 값을 복사하는 것

  • 예시) movq (%rcx), %rax
  • rcx 레지스터에 저장된 값을 rax 레지스터에 값을 복사
  • (%rcx) : rcx 라는 register 의 메모리 주소에 들어있는 값
  • %rax : rax 라는 라는 레지스터의 메모리 주소
  • Displacement (형태2) : Mem[Reg[R] + D]
    해석 ) 레지스터 R 이 가리키는 메모리에서 D 바이트 만큼 떨어진 곳에 위치한 메모리에 access

  • 예시) movq 8(%rbp), %rdx
  • 레지스터 rbp 의 메모리 주소에서 8 만큼 더 주소를 이동해서 그곳에 있는 메모리에 저장된 값을 rdx 레지스터에 복사

예제

  • 두 메모리에 있는 값을 swap 하는 swap 함수임
  • 왼쪽 c언어 코드를 컴파일 해보면 오른쪽과 같은 어셈블리 코드가 나온다.

상세분석

전체그림

register 포인터

  • 힘수 호출시 rdi, rsi 레지스터 (레지스터 = 포인터) 각각 특정 메모리를 가리키게 함
    ( c언어 코드를 보면 포인터 xp, yp 가 있는데 각각 rdi, rsi 로 변한 것이다. 드리고 각 레지스터 rdi 와 rsi 가 포인터처럼 메모지 주소를 저장한다 )

어셈블리의 swap 연산 수행과정

주석처리 # 가 되어있는 부분이 c언어 코드로, 이게 어셈블리 코드로 바뀐 것이다!

movq (%rai), %rax : rai 레지스터 포인터가 가리키는 메모리에 접근해서 얻어온 값을 rax 가 가리키는 메모리에 값을 할당
movq (%rsi), %rdx : 위 코드와 동일한 원리
movq %rdx, (%rdi) : 동일한 원리
movq %rax, (%rsi) : 동일한 원리

=> 연산 결과, rdi 와 rsi 가 가리키는 각 메모리의 값이 swap 된다!

Complete Memory Addressing

형태 : 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) 를 더해주면 된다!


Address Computation

  • 형태 : 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 와의 차이점)


Two Operand 를 취하는 명령어들

  • 위와 같은 명령어들은 2개의 Operand 를 가지고 계산한다.

One Operand 를 취하는 명령어들

  • 반면 위와 같은 명령어들은 1개의 Operand 를 가지고 계산한다.
    Source 가 없고 destination 만 존재한다.

예제1

  • addq %rdx, %rax : %rdx 와 %rax 를 더함
  • salq $4, %rdx : %rdx 에 왼쪽 쉬프트 연산을 4번 수행. 즉 16이 곱해짐
  • imulq %rcx, %rax : %rcx 와 %rax 를 곱한 결과값을 %rax 에 할당

x86-64 의 아키텍쳐 시스템

  • 16개의 레지스터가 존재 (%rax, %rbx, ..)

  • 위 레지스터외에도 instrction pointer 와 condition code 라는 레지스터들이 또 있다.


Condition Codes

  • 1bit 짜리 레지스터들
    • CF : unsigned Carry flag ( carry bit )
    • SF : singed Sign Flag ( 부호 bit )
    • ZF : Zero Flag ( zero bit )
    • OF : OverFlow Flag ( overflow bit )

=> CF : unsigned 숫자들을 64bit 짜리로 표현하는데 어떤 계산을 했을때의 결과값이 64bit 가 표현할 수 있는 범위를 벗어날 떄 (오버플로우가 발생할 때) 넘치는 65번째 1이 생기면 CF bit 가 올라간다.

=> SF : signed 숫자에 대해 64bit 시스템에서의 부호비트 (숫자가 0보다 작으면 부호비트가 1로바뀜)

=> ZF : 숫자가 0일때 비트가 1로바뀜

=> OF : 오버플로우가 발생할떄 1로바뀜


Compare 명령어 - cmpq

  • cmpq b, a : 결과값이 그 어떤 레지스터에도 저장되지 않지만, condition codes(CF, SF, ZF, OF) 에 저장된 값들을 최신화시킨다.

(=> b, a 둘다 source 이다. destination 이 없어서 결과값이 없는 것이다! )

CF : MSB(Most Significant Bit) 에서 오버플로우 발생시 1로바뀜
SF : a - b < 0 일때 1로 바뀜
ZF : a == b 일떄 1로 바뀜
OF : signed 2의보수 시스템에서 오버플로우 발생시 1로바뀜


test 명령어

  • testq b, a : compare 명령어와 동일하나, 차이점은 ZF 와 SF 만 최신화한다!

setX 명령어

종류

  • setX : condition 코드가 1일때 destination 을 1로 설정한다.

ex) sete : condition 코드 ZF 가 1일떄 destination 으로 1을 설정한다.


  • 각 레지스터에 대한 LSB(Least Significant Byte) 이자, destination 이 된다.

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

Conditional Branch

  • Conditional Branch : 테스팅 결과에 따라 분기 여부를 결정하는 분기 명령어이다.

Jumping - jX 명령어

  • c언어의 goto 와 비슷. 주어진 조건을 만족하면 해당 메모리 공간으로 점프를 뛰는것

예제

c언어 코드 : absdiff(absolute difference) : 두 숫자의 절댓값 크기 차이를 리턴

어셈블리 코드

jle .L4 => %rsi 가 %rdi 보다 작을경우 ".L4" 구문으로 점프뛴다.
반대로 작지 않다면 그냥 아래 코드들을 줄줄이 차례대로 수행한다.

C언어 코드 상세분석

conditional branch 에서는 왼쪽과 같은 코드를 자동으로 오른쪽처럼 변환시켜준다.


Conditional Move

앞서 살핀 코드를 다시 살펴보면 아래와 같다.
이 conditional code 를 살펴보면 if문으로 검사를 하고 jump 를 할지말지 여부를 결정한다. 이런 방식은 jump 한다는 과정 자체가 굉장히 비효울적이다.

예제

어셈블리 코드 자세히 보기

  • 반면 위와 같은 conditional move 코드는 jump 과정이 없어서 보다 효율적이다.

( 몰론 대신에 %rsi - %rax 와 %rax - %rsi 값을 모두 계산해야 한다는 점이 있긴하다. 그래도 jump 과정이 생략되었다는 점에서 효율적이다! )

profile
https://haon.blog

1개의 댓글

comment-user-thumbnail
2024년 1월 29일

잘 봤습니다!

답글 달기