혼자 정리

[CSAPP] 3.4 정보 접근하기 ~ 3.5 산술연산과 논리연산 본문

CSAPP정리

[CSAPP] 3.4 정보 접근하기 ~ 3.5 산술연산과 논리연산

tbonelee 2021. 6. 22. 16:49

x86-64 CPU는 다음과 같이 64비트 값 저장 가능한 범용 레지스터를 16개 가지고 있다.

registers

  • 맨 앞의 붙은 r은 register의 의미로 64비트를 뜻한다
  • 맨 앞에 e가 있는 경우 32비트를 의미
  • 기본적으로 그림에서 보듯이 인스트럭션은 16개의 레지스터에 있는 여러 크기의 하위 바이트 데이터에 대해 연산할 수 있다.
  • 64비트 연산시 레지스터 전체에, 32비트 연산시 하위 4바이트에, (위 그림에는 없지만) 16비트 연산시 하위 2바이트에, 8비트 연산시 하위 1바이트에 접근.

레지스터에서 1,2,4,8바이트를 사용할 수 있는데 8바이트를 사용하는 경우를 제외하고 전체 레지스터를 사용하지 않는다. 그러한 경우 레지스터의 남는 바이트들에 대해서 다음과 같이 처리한다.

  • 1 or 2바이트 생성 : 나머지 바이트 변경 없이 유지
  • 4바이트 생성 : 나머지 상위 4바이틀 0으로 설정

이 중 %rsp는 스택 포인터로 런타임 스택의 끝 부분을 가리킨다. 따라서 특정 인스트럭션들 위졸 이 레지스터를 읽거나 기록한다.
나머지 레지스터는 이보다는 조금 더 자유롭다.

cf) gdb를 통해 메인 함수의 지역 변수가 실제 스택에 저장되어 있는지 확인해 보았다.

int main(){
    long long k = 48 // 16 * 3
    k = 10;
}

gdb_rsp

breakpoint는 k = 10;을 수행하기 직전 단계로 설정했다.
스택의 끝 부분을 가리키는 $rsp0x7fffffffe4d0의 주소를 가지고 있고 k가 저장되어 있는 주소는 0x7fffffffe4c8의 주소를 가지고 있다.
지역 변수 k가 스택의 끝 부분보다 낮은 주소를 가지고 있으므로 기대하던 것과 같은 것으로 보인다.
실제 k의 주소에 저장되어 있는 비트를 참조하면 0x0000000000000030으로 16 * 3 = 48의 값이 잘 저장되어 있다.


3.4.1 오퍼랜드 식별자(specifier)

인스트럭션의 경우 인스트럭션을 수행할 오퍼랜드를 한 개 이상 가지게 된다.
오퍼랜드는 값을 참조해서 연산을 수행할 소스(source)가 될 수도 있고, 연산 수행의 결과를 저장할 목적지(destination)가 될 수도 있다.

보통 소스 값은 상수로 직접 주어지거나 레지스터에 있는 값을 참조할 수 있다.
결과값은 레지스터에 저장하거나 메모리에 직접 저장할 수 있다.

이러한 오퍼랜드는 세 가지 유형으로 구분할 수 있다.

  1. Immediate : 상수값을 의미
    • '$'기호 다음에 C표준 형태의 정수로 나온다.
    • $-577이나 $0x1F같은 형태.
    • 1,2,4바이트 중 하나로 인코딩된다.
  2. Register : 레지스터의 내용을 의미
    • 16개의 레지스터들의 하위 8바이트(전체), 4바이트, 2바이트, 1바이트를 의미한다.
    • 보통 %rax, %r13처럼 작성
  3. Memory : 특정 메모리 주소가 주어졌을 때 해당 메모리 주소에 들어있는 8개의 연속적인 값을 의미
    • (%rax)와 같은 형태 : 괄호가 값 참조와 비슷한 의미

ex) movq srcs,dest인스트럭션을 사용하는 경우(srcs에서 dest로 데이터 이동; q는 8바이트=64비트 데이터를 의미)

movq_operands

  • immediate값은 상수이므로 dest가 될 수 없다.
  • 하드웨어 기능 구현의 편의상 메모리에서 메모리로 직접 값 복사하는 것은 불가능. (mem -> reg && reg -> mem해야 함)

메모리 주소 지정 방식

  • Normal : 표기 - (R) -> 의미 - Mem[Reg[R]]
    • 여기서 레지스터 R은 메모리 주소를 의미.
    • 뒤에 Mem[Reg[R]]은 메모리에서 레지스터 R이 담고 있는 주소를 가지고 참조해서 값을 반환하는 것.
    • C의 포인터 역참조를 생각하면 된다.
    • movq (%rcx), %rax
  • Displacement(이동) : 표기 - D(R) -> 의미 - Mem[Reg[R]+D]
    • R은 위와 같은 의미
    • 주소가 D의 오프셋만큼 이동(양수든 음수든)
    • movq 8(%rbp), %rdx
  • Most General Form : 표기 - D(Rb,Ri,S) -> 의미 - Mem[Reg[Rb]+S*Reg[Ri]+D]
    • D: 상수항으로 1,2,4바이트가 올 수 있음 <- 오프셋의 의미
    • Rb: Base register: 16개의 정수 레지스터 중 아무 거나
    • Ri: Index register: %rsp을 제외한 모든 레지스터
    • S: Scale: 1,2,4,8이 올 수 있음(인덱스를 하나씩 셀 때 데이터 크기만큼 넘겨야 하는데 그 데이터 크기가 보통 1,2,4,8이 오니까..)

- 메모리 주소 계산 예시

다음의 주소값을 가정

| %rdx | 0xf000 |
| %rcx | 0x0100 |

그러면 다음에서 명령과 계산 결과를 알 수 있다.

Expression 주소 계산 주소
0x8 (%rdx) 0xf000 + 0x8 0xf008
(%rdx, %rcx) 0xf000 + 0x100 0xf100
(%rdx, %rcx, 4) 0xf000 + 4*0x100 0xf400
0x80(,%rdx,2) 2*0xf000 + 0x80 0x1e080
cf) base register도 생략 가능(다른 것도 마찬가지지만..)

movq 인스트럭션 예시를 통해 메모리 주소 지정 방식 살펴보기

void swap(long *xp, long *yp)
{
    long t0 = *xp;
    long t1 = *yp;
    *xp = t1;
    *yp = t0;
}
swap:
    movq    (%rdi), %rax
    movq    (%rsi), %rdx
    movq    %rdx, (%rdi)
    movq    %rax, (%rsi)
    ret

swap

  • %rdi%rsi는 각각 첫번째 인자와 두번째 인자 주소를 갖는 레지스터.
  • 이 값은 함수가 실제로 실행되기 전에 설정된다. (함수 caller에 의해)
  • %raxt0값(주소 x), %rdxt1값(주소 x)을 가지고 있음
    swapex1
    swapex1
    swapex1
    swapex1
    swapex1

주소 계산 인스트럭션

  • leaqSrc, Dst
    • Src는 주소 지정 방식으로 되어 있는 표현
    • Dst는 레지스터여야 함. Dst를 Src가 나타내는 주소로 설정하는 인스트럭션.
  • 어디에 쓰이는지?
    • 메모리 참조하지 않고 주소를 계산하기
      • ex) p = &x[i];의 어셈블리어 버전
    • x+k*y형태의 산술 표현을 계산
      • k = 1,2,4,8
  • 주소를 할당하는 것이지 값을 할당하는 것이 아님에 유의!
  • ex)
    다음 예시에서 leaq를 사용.(아마 같은 8바이트 데이터라 주소 연산을 활용해서 최적화하는 느낌?)
    (lea인스트럭션을 쿼드워드에 수행)컴파일러가 다음의 어셈블리어로 변환
  • leaq (%rdi,%rdi,2), %rax # t <= x+x*2 salq $2, %rax # return t<<2
  • long m12(long x) { return x*12; }
  • %rdi에 있는 주소값(정수)에 (%rdi값*2)한 값을 더하면 3만큼 곱한 값이 구해지고
  • salq를 통해 2만큼 좌측 비트 시프트(2^{2} = 4만큼 곱한 효과)
  • 12만큼 곱한 효과

산술 계산 오퍼레이션

오퍼랜드 두 개짜리 인스트럭션

형식 계산 특이사항
addq Src,Dest Dest = Dest + Src
subq Src,Dest Dest = Dest - Src
imulq Src,Dest Dest = Dest*Src
salq Src,Dest Dest = Dest << Src shlq로 불리기도 함
sarq Src,Dest Dest = Dest >> Src 산술 우측 시프트
shrq Src,Dest Dest = Dest >> Src 논리 우측 시프트
xorq Src,Dest Dest = Dest ^ Src
andq Src,Dest Dest = Dest & Src
orq Src,Dest Dest = Dest Src
  • 형식에서는 소스가 먼저 오고 목적지가 뒤에 오지만 실제 계산은 반대 순서인 것에 주의!
  • 비트 수준 연산에서는 signed, unsigned 구분이 없으니까 여기서도 구분 없이 연산

오퍼랜드 한 개짜리 인스트럭션

형식 계산
incq Dest Dest = Dest + 1 increment
decq Dest Dest = Dest - 1 decrement
negq Dest Dest = -Dest negation
notq Dest Dest = ~Dest not

산술 표현 예시

long arith(long x, long y, long z)
{
    long t1 = x+y;
    long t2 = z+t1;
    long t3 = x+4;
    long t4 = y * 48;
    long t5 = t3 + t4;
    long rval = t2 * t5;
    return rval;
}

어셈블리어로 변환하면 다음과 같다.

arith:
    leaq    (%rdi,%rsi), %rax   # t1
    addq    %rdx, %rax       # t2
    leaq    (%rsi,%rsi,2), %rdx
    salq    $4, %rdx       # t4
    leaq    4(%rdi,%rdx), %rcx   # t5
    imulq   %rcx, %rax     # rval
    ret
Register 어떻게 쓰였는지
%rdi 인자 x
%rsi 인자 y
%rdx 인자 z
%rax t1, t2, rval
%rdx t4
%rcx t5
  • leaq : 주소 계산
  • salq : 비트 좌측 시프트
  • imulq : 곱셈
  • 우리가 작성한 코드와 실제 컴파일된 어셈블리어는 딱 들어맞지 않는다. (심지어 직접적인 t3연산은 찾을 수 없음)
  • 컴파일러가 최적화 과정을 거치기 때문
  • $\therefore$ 결과는 의도한 대로 나오지만 실제 기계어 수준 인스트럭션은 우리가 생각하는 대로 이루어지지 않을 수 있다는 점이 요점.