Phase3는 문제 유형이 2개인데, 이 포스트에서는 입력받는 값이 3개(%d %s %d
)인 경우를 다룬다.
그러면 한 번 phase_3의 어셈블리 코드를 해석해보자.
Phase 3
는 어셈블리 코드를 분석하여 C언어의 switch
구문과 유사한 구조를 찾아내는 것이 핵심이다.
함수 초반부에서 scanf
함수를 호출하여 사용자 입력을 받는다.
<+35>
: `mov 0x184f(%rip),%rsi명령어를 통해 포맷 문자열의 주소를
%rsi 레지스터에 로드한다. GDB로 해당 주소(
0x555555402be6)의 문자열을 확인하면 **
%d %c %d`** 임을 알 수 있다. %rsi
는 함수에 들어가는 인자이므로 이 값이 <+42>
의 scanf 함수(<__isoc99_scanf@plt>
)로 들어감을 확인할 수 있다. 입력된 세 값(정수, 문자, 정수)은 스택 포인터(%rsp
)를 기준으로 한 특정 위치에 저장된다. 이 값들은 <+20>
에서 <+30>
사이의 명령어들을 통해 파악할 수 있다. %rdx
: 첫 번째 정수(%d
)가 저장될 주소 (%rsp + 16
)
%rcx
: 문자(%c
)가 저장될 주소 (%rsp + 15
)
%r8
: 두 번째 정수(%d
)가 저장될 주소 (%rsp + 20
)
각 데이터 타입의 크기(정수 4바이트, 문자 1바이트)를 고려하면, 이 명령어들은 스택에 세 개의 변수를 위한 공간을 마련하고 각 변수의 시작 주소를 scanf
의 인자로 넘겨주는 것과 동일하다.
예를 들어 사용자가 2 n 4
를 입력하면, scanf
함수가 실행된 후 스택에는 다음과 같이 값이 저장된다.
<+47>
, <+50>
: scanf
의 반환 값(%rax
)이 2
보다 큰지 비교한다 (cmp $0x2,%eax
, jle
). scanf
는 성공적으로 입력받은 인자의 개수를 반환하므로, 입력값은 반드시 3개여야 한다.코드의 다음 부분은 첫 번째로 입력된 정수에 대한 조건을 검사한다.
<+52>
, <+57>
: cmp $0x7,0x10(%rsp)
명령어는 스택에 저장된 첫 번째 정수가 7보다 큰지 비교하고 크면은 폭탄이 터진다(ja <phase_3+331>
,ja
는 unsigned 비교).따라서 첫 번째 정수는 0에서 7 사이의 값이어야 한다.
현재까지의 답: (0-7) (char) (정수)
<+63>
: %eax
에 %rsp+10
, 첫 번째 정수 x
의 주소값을 넘겨준다.<+67>
: lea 0x1849(%rip),%rdx
명령어로 특정 주소를 %rdx
에 로드한다. 이 주소는 <+74>
~<+81>
을 보면은 분기할 주소들의 목록, 즉 점프 테이블의 시작 주소임을 알 수 있다. 해당 주소에 16진수, 워드 단위로의 값출력을 하면은 다음과 같은 값이 나온다. <+74>
: movslq (%rdx,%rax,4),%rax
명령어가 실행된다. 여기서 봐야할 점은 다음과 같다.%rax
에는 <+63>
을 통해 첫 번째 입력 정수 값이 들어있다.
(%rdx, %rax, 4)
는 rdx + rax * 4
주소를 의미한다. 즉, 시작 주소(<+67>
)에서 첫 번째 정수 값만큼의 인덱스를 이용해 이동한다. 이로 $0x555555402c00
은 배열로 유추할 수 있다.
결과적으로, 이 명령어는 첫 번째 정수 값에 해당하는 분기 주소를 테이블에서 읽어와 %rax
에 저장한다.
만약 첫 번째 정수 x
가 2
이면은 %rax
에는 *(0x555555402c00 + 4 x 2)
의 값이 sign extension되어 0xffffffffe80b
이 저장된다.
<+78>
: add %rdx, %rax
명령어는 이 오프셋 값에 점프 테이블의 시작 주소(%rdx
)를 더하여 실제 실행할 코드의 주소를 계산한다.x
가 2
일 경우에는 0x555555402c00 + 0xffffffffe80b = 0x55555540140b
이 된다. 이 값이 %rax
에 저장된다.<+81>
: jmpq *%rax
명령어는 %rax
에 저장된 주소로 프로그램 카운터를 이동시킨다.이러한 구조는 C언어의 switch
문이 컴파일된 전형적인 형태이다. 첫 번째 정수 x
의 값(0~7)에 따라 8개의 다른 코드 블록으로 분기하게 된다.
첫 번째 정수 x
가 2
일 경우일 때의 분기를 보도록 하자. 0x55555540140b
은 <Phase_3 +158>
에 해당한다.
<+158>
:%eax
에 0x69
의 값을 넣는다.<+163>
: *(rsp+20)
(두 번째 정수 y
)과 0x2f2
가 동일한지 판단한다. 동일하면은 <+341>로 분기하고, 그렇지 않으면은 폭탄이 터진다.따라서 x
가 2
일 때, 두 번째로 입력하는 정수 y
는 0x2f2
(754
)이다.
이 명령어에선 %al
(rax
레지스터에서 하위 8비트 부분)과 스택에 저장된 문자(*(%rsp+15)
)를 비교한다. 이전 단계에서 switch
문의 각 case
블록은 %eax
에 고유한 값을 저장했다 (<+158>
).
따라서 입력된 문자는 해당 case
에서 지정한 값의 ASCII 코드와 일치해야 한다. 예를 들어, x=2
의 경우 문자는 반드시 ASCII 코드 0x69
, 즉 'i'여야 한다.
앞선 분석 과정을 종합하면, Phase 3
의 핵심 로직은 C언어의 switch
문과 동일한 구조임을 알 수 있다.
switch(x)
문의 x
처럼 분기할 case
를 선택하는 역할을 한다. 이 값은 0에서 7 사이로 제한된다.0x555555402c00
주소는 점프 테이블, 즉 각 case
로 점프하기 위한 주소 오프셋을 담은 배열을 가리킨다.case
의 로직으로 분기한다.이러한 메커니즘을 통해, 첫 번째 정수 x
의 값에 따라 분기되는 8개의 주소는 다음과 같이 정리할 수 있다.
<+90>
<+124>
<+158>
<+192>
<+223>
<+250>
<+277>
<+304>
그리고 이 분기에는 두 번째 정수 입력과 Char 입력 조건이 작성되어 있다. 정리해보면 답은 다음과 같다.
답:
0 t 1351 j 923
2 i 754
3 d 178
4 n 307
5 k 432
6 f 794
7 b 343