제어 흐름도 논하는 어셈블리 명렁어 [CSAPP Chap 3]

어셈블리어를 알아두는건 정말정말 실제로 열어볼까 말까겠지만 이런 단계가 기저에 있다는 것을 알아두는 것은 도움이 된다고 한다. 현재의 개발 형태들이 보다 저급을 추구하는 형태의 고점이 C라는건 맞는데 요즘 돈 버는 서비스들이 거기까지 가는지 정말 궁금하다.


그럼 오고가는 명령어인 mov에 대해 알아보자. mov + 8바이트인 movq는 3가지 채널을 활용 할 수 있다.

  • 즉시 값을 레지스터에 부여하기 : movq  $0x123, %rax
    • $는 온전히 그 숫자 값을 말한다.
    • 숫자 0x123을 %rax에 넣는다.
  • 레지스터의 값을 레지스터에 부여하기 : movq  %rdi, %rax
    • %rdi의 값을 %rax에 넣기
  • 메모리의 값을 레지스터에 부여하기 : movq  (%rdi), %rax
    • ()는 메모리 주소를 뜻한다.
    • %rdi 내의 값이 주소인 메모리 위치로 가서, 내용을 %rax로 가져오기

모든 내용은 역 방향도 성립한다.

void store_value(long *xp, long y)
{
	*xp = y;
}

xp가 %rdiy가 %rsi에 대응된다고 해보자. 그러면 movq %rsi, (%rdi) 로 나타낼 수 있다.

 

실제 사칙 연산으로는 덧셈, 뺄셈, 곱셈, 논리 연산 정도가 있다. 각각

  • add
  • sub
  • imul
  • and or xor

add %rsi, %rdi는 rdi = rdi + rsi 와 동일하다.

long calculate(long x, long y, long z) {
    long result = x + y - z;
    return result;
}
movq  %rdi, %rax  # rax = x
addq  %rsi, %rax  # rax = x + y
subq  %rdx, %rax  # rax = x + y - z
ret               # return rax

둘은 동일하다.

 

제어 흐름 논하기

어셈블리에는 if, while 같은게 없고 jump 라는 아주 원초적인 기능만 가지고 있다. 이 과정은 두 단계로 이루어진다.

  • 비교 (Compare) : cmp와 같은 명령어로 두 값을 비교한다. 이 결과는 특별한 레지스터인 조건 코드에 저장된다.
  • 점프 (Jump) : je (같으면 점프), jg (더 크면 점프) 같은 조건부 점프 명령어가 조건 코드를 확인하고, 조건이 맞으면 지정된 위치로 점프한다.
long abs_diff(long x, long y) {
    if (x > y) {
        return x - y;
    } else {
        return y - x;
    }
}

이 코드는 x - y와 y - x에 대응하는 내용이 어셈블리 코드로 사전에 작성이 된다. 비교 결과에 따라 어느 쪽으로 점프할 지 결정하게 된다.

abs_diff:
    cmpq    %rsi, %rdi      # y와 x를 비교 (내부적으로 x - y 연산)
    jle     .L_else         # 만약 x가 y보다 작거나 같다면(jle), .L_else로 점프
    movq    %rdi, %rax      # (if 블록) rax = x
    subq    %rsi, %rax      # rax = x - y
    jmp     .L_done         # 계산이 끝났으니 .L_done으로 점프
.L_else:
    movq    %rsi, %rax      # (else 블록) rax = y
    subq    %rdi, %rax      # rax = y - x
.L_done:
    ret                     # rax에 저장된 값을 반환
x > y에 첫 번째 블록을 실행하라고 지시했던 C 코드와 비교 할 때 jle(작거나 같을 때 점프) 를 사용한 점에 주목해야 한다.

Fall-Through를 준수하기 위한 것인데, if-else와 같은 조건부 점프를 만나면 특정되기 전까진 작업을 준비 할 수 없기 때문에 분기 예측을 하게 된다. 예측이 성공하면 정말 좋지만 실패하면 잘못 준비된 걸 전부 버리고 다시 해야한다. 여기서 성능 저하가 발생한다.

즉, 더 자주 실행될 것 같은 코드 경로를 점프가 일어나지 않는 Fall-Through에 배치하려는 것이 컴파일러 행위의 기본 값이다. 그래서 if-else는 보통 if가 더 일반적이라고 여기기 때문에, 상대적으로 덜 일반적인 else에서 점프하도록 하는 것이다.

분기 예측 성공률이 높아지면 전체 프로그램의 실행 속도도 빨라진다.

 

점프 테이블

if-else 보다 더 많은 분기의 switch문을 위해 컴파일러는 점프 테이블이라는 최적화 기법을 사용한다.
마치 책의 목차 처럼, 원하는 내용이 n 페이지에 있다는 것을 확인 한 후, 한 번에 해당 페이지로 점프 할 수 있다.

 

 

프로시저

함수를 호출하고 돌아오는 과정은 스택을 사용해서 정교하게 관리된다.

main이 sum을 호출한다고 상상하면, 스택은 main의 수행하던 내용을 돌아올 수 있는 복원 지점의 의의에 맞게 저장한다.
이 때 복원 지점을 스택 프레임이라 부른다.

  • 몇 번 줄, 저장되어있던 변수 두 개 다 들어가있다.
  • main이 sum을 호출하는 call sum 호출 시 call 다음 줄의 명령어 주소를 스택에 저장한다. 그리고 ret 명령어 실행시 스택에 저장한 그 복귀 주소를 꺼내서 다시 하던 걸 하게 된다.