이 연습 문제에서는 x86-64 어셈블리 언어에서 영감을 받은 작은 언어의 문법과 실행 모델을 정의해요.
목표는 제한된 환경 안에서 코드가 어떻게 작성되고, 파싱되고, 실행되는지를 형식화하는 것이에요.
어셈블리 코드는 특별한 구조 안에 작성해요:
let program := assemble!(
-- assembly code here
)
이 구조는 어셈블리 코드를 실행하고 값을 반환하는 함수를 시뮬레이션하는 프로그램을 만들어야 해요.
이 프로그램은 다음 문법으로 호출하는데, 여기서 a, b, c 등은 인자예요:
let result := program(a, b, c, ...)
모든 인자와 반환값은 _64비트 부호 있는 정수_예요. 인자는 최대 _6개_까지 허용돼요.
반환값과 모든 인자는 레지스터로 전달돼요. 여러분의 언어는 다음 레지스터를 지원해야 해요:
| 레지스터 | 역할 |
|---|---|
rdi |
첫 번째 인자 |
rsi |
두 번째 인자 |
rdx |
세 번째 인자 |
rcx |
네 번째 인자 |
r8 |
다섯 번째 인자 |
r9 |
여섯 번째 인자 |
rax |
반환값 |
레지스터는 함수에 인자를 전달하는 데 사용되지 않는 한 기본값 0으로 시작해요.
예를 들어 다음 프로그램에서 rdi는 10으로, rsi는 -20으로 초기화되고, 이를 제외한 모든 레지스터의 값은 0이에요:
program(10, -20)
프로그램의 반환값은 항상 rax에 저장돼요.
레지스터 이름은 대소문자를 구분하지 않아요.
즉, rax, RAX, rAx는 모두 같은 레지스터를 가리켜요.
어셈블리 코드의 각 줄은 다음 두 가지 형태 중 하나예요:
어떤 명령어에 의해 바뀌지 않는 한, 프로그램의 실행 흐름은 선형으로 진행돼요. 즉, 앞 줄이 완전히 실행된 다음에 그다음 줄이 실행돼요.
레이블의 문법은 다음과 같아요:
<label>:
레이블은 명령어가 사용할 수 있도록 코드의 특정 위치를 표시하는 것 외에는, 어떤 레지스터의 값도 바꾸지 않고 프로그램에도 아무런 영향을 주지 않아요.
레이블은 대소문자를 구분해요.
즉, Start, start, sTart는 모두 서로 다른 레이블이에요.
대부분의 명령어는 다음과 같은 문법을 가져요:
<opcode> <destination>, <source>
opcode는 destination과 source 피연산자의 값을 사용해 실행할 연산을 나타내요.
연산 결과는 destination 피연산자에 저장돼요.
예를 들어, add 명령어는 destination과 source 피연산자의 값을 더해 그 결과를 destination 피연산자에 저장해요.
destination 피연산자는 항상 레지스터인 반면, source 피연산자는 레지스터일 수도 있고 리터럴 숫자일 수도 있어요.
다음은 여러분의 프로그램이 지원해야 하는 명령어 목록이에요:
| opcode | 수행하는 연산 |
|---|---|
mov |
destination := source |
add |
destination := destination + source |
sub |
destination := destination - source |
mul |
destination := destination * source |
div |
destination := destination / source |
xor |
destination := destination ^^^ source |
and |
destination := destination &&& source |
or |
destination := destination ||| source |
shl |
destination := destination <<< source |
shr |
destination := destination >>> source |
이 외에도, 여러분의 프로그램은 두 개의 피연산자를 갖는 cmp 명령어를 지원해야 해요.
이 명령어는 어떤 레지스터도 바꾸지 않고, 대신 destination과 source 피연산자를 비교해 프로그램의 내부 상태를 다음 세 가지 중 하나로 설정해요:
destination > source이면 큼
destination == source이면 같음
destination < source이면 작음
이 상태를 내부적으로 어떻게 관리할지는 자유예요.
프로그램의 흐름을 바꿔 실행을 코드의 다른 지점으로 옮기는 명령어도 있어요. 이런 명령어를 점프 명령어라고 해요.
이 명령어들은 모두 피연산자를 하나만 받는데, 그 피연산자는 점프의 대상, 즉 실행이 이어질 코드의 지점을 나타내는 레이블이에요:
<opcode> <label>
jmp 명령어는 항상 대상 레이블로 점프해요.
다른 점프 명령어들은 프로그램의 내부 상태가 특정 값으로 설정된 경우에만 점프해요.
그래서 보통 cmp 명령어 뒤에 와요.
다음은 여러분의 프로그램이 지원해야 하는 점프 명령어예요:
| 명령어 | 수행하는 연산 |
|---|---|
jmp |
무조건 점프해요. 점프는 항상 실행돼요 |
je |
내부 상태가 _같음_이면 점프해요 |
jl |
내부 상태가 _작음_이면 점프해요 |
jg |
내부 상태가 _큼_이면 점프해요 |
명령어 opcode는 두 개의 피연산자를 갖든 하나의 피연산자를 갖든 대소문자를 구분하지 않아요.
즉, add, ADD, aDd는 모두 같은 명령어를 가리켜요.