손으로 작성한 어셈블리는 반복투성이예요. 같은 짧은 명령어 묶음이 거의 변형 없이 몇 번이고 다시 나타나는데, 반복되는 코드를 줄여 줄 추상화는 거의 없어요. 게다가 어셈블리로 코딩하려면 뚜렷한 의미도 없는 값들을 직접 관리해야 해요. 이름이 붙은 매개변수도 없고, 바탕이 되는 기계의 세부 사항 대부분을 프로그래머가 직접 다뤄야 하죠.
이 트랙에서 사용하는 어셈블러인 NASM은 이 두 문제를 모두 해결하는 데 도움이 되는 몇 가지 도구를 제공해요. 이 도구들은 명령어가 프로세서에 도달하기 전인 _어셈블 시점_에 실행돼요. 기계어로 번역되기도 전에 소스 텍스트를 어떻게 다시 쓸지 어셈블러에게 알려 주는 거예요.
이 도구들 중 가장 중요한 것이 매크로인데, 어셈블러를 위한 조리법이라고 생각하면 돼요. 매크로는 사용된 위치에서 펼쳐지는데, 마치 그 자리에 코드를 손으로 써 넣은 것과 같아요.
매크로는 여러 리터럴 토큰을 매개변수로 받을 수 있어서 여러 상황에서 재사용할 수 있어요.
하지만 함수는 아니에요. call도 없고, ret도 없고, 코드의 다른 지점으로 제어를 옮기는 일도 없어요.
가장 단순한 정의는 %define으로 값이나 짧은 표현식에 이름을 붙이는 거예요.
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
이후에 나오는 WIDTH는 모두 어셈블 전에 8로 바뀌어요.
치환이 텍스트 단위로 이루어지기 때문에, 피연산자 안이든 다른 정의 안이든 이름이 나타나는 곳마다 그대로 바뀌어요.
숫자 _상수_에는 %define 대신 equ를 쓰는 게 좋아요.
%define으로 선언한 한 줄 매크로는 사용될 때만 펼쳐지고, 다시 지정하거나 정의를 해제할 수 있어요.
반면 equ 상수는 정의된 지점에서 평가되고 절대 바뀌지 않아요.
%define은 이름이 있는 매개변수도 받을 수 있어서 작은 인라인 함수처럼 동작해요.
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
전체 본문과 각 매개변수를 모두 괄호로 감싸요.
%define square(x) x*x라고 하면 square(1+1) 호출은 1+1*1+1, 즉 3으로 펼쳐져요.
%define square(x) ((x)*(x))라고 쓰면 같은 호출이 ((1+1)*(1+1)), 즉 4로 펼쳐져요.
%define으로 정의한 이름은 대소문자를 구분해요.
%idefine을 쓰면 이름의 대소문자를 구분하지 않게 할 수 있는데, 필요할 때만 쓰면 돼요.
한 줄 매크로는 코드의 여러 부분에 의미를 부여할 때 유용해요. 주의해서 사용하면 가독성이 좋아지고 오타가 날 가능성도 줄어들어요. 예를 들어 함수 안에서 특정 용도로만 쓰이는 레지스터는 그 용도로 이름을 정의할 수 있어요.
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
여러 줄이든 코드 전체 묶음을 %macro로 담고 %endmacro로 닫아요.
이름 뒤의 숫자는 매크로가 받는 인자의 개수를 나타내며, 0일 수도 있어요.
인자를 넘길 때는 쉼표(,)로 구분해요.
%macro example_macro 0
...
%endmacro
여러 줄 매크로의 인자에는 이름이 없어요.
넘겨진 순서대로 %1, %2 등으로 가리켜요.
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
인자는 매크로 본문에서 수행하는 연산에 맞는 올바른 구문이어야 한다는 점에 유의해요.
여기서 mult_two eax, ecx와 mult_two r8, r9는 둘 다 어셈블되지만, mult_two eax, cl은 안 돼요. imul eax, cl이 정의되어 있지 않기 때문이에요.
일반 레이블을 정의하는 매크로는 두 번 사용되는 순간 깨져요. 그러면 레이블도 두 번 정의되기 때문이에요.
여러 번 호출될 수 있는 매크로 안에서 레이블을 쓰려면 레이블 앞에 %%를 붙여야 해요.
그러면 어셈블러가 펼칠 때마다 이 레이블에 서로 다른 고유한 이름을 붙여요.
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
times를 사용하면 한 줄짜리 코드 블록을 반복할 수 있어요.
메모리에 같은 값을 반복해서 선언할 때 특히 유용해요.
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
여러 줄 코드를 반복할 때는 대신 %rep를 사용할 수 있어요.
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
반복 횟수는 %rep 바로 뒤에 쓰고, 반복 블록을 닫는 %endrep가 있다는 점에 유의해요.
실행 시점의 루프가 카운터를 쓰는 것처럼, %rep 블록 안에서 어셈블 시점 카운터를 쓰는 일도 흔해요.
어셈블 시점 정수 상수는 equ로 선언할 수 있지만, 어셈블 시점 정수 _변수_에는 대신 %assign을 써요.
%define과 달리 %assign은 즉시 숫자로 평가되고 다시 지정할 수 있어요.
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
전처리기가 실행되고 나면 루프는 사라지고, 즉시값 0, 1, 2, 3이 박혀 있는 네 개의 add 명령어만 남아요.
이것은 어셈블러가 수행하는 루프 펼치기예요. 카운터 레지스터도 분기도 없이 그냥 직선형 코드만 남아요.
전처리기에는 여러 가지 조건문을 사용할 수 있어요. 이들은 여러 경로 중 하나를 선택하고, 조건이 참일 때만 제자리에서 본문을 펼쳐요.
%if ... %elif ... %else ... %endif는 숫자 검사 결과에 따라 분기를 선택해요.
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
매크로는 보통 별도의 파일에 두고, 필요한 각 소스에서 %include로 불러와요.
%include "patterns.mac"
지정한 파일은 어셈블 전에 그 지점에 그대로 삽입되는데, 마치 그 자리에 내용을 직접 타이핑한 것과 똑같아요. 이렇게 하면 정의를 한 곳에 모아 두고 여러 소스 파일이 함께 쓸 수 있어요.
매크로 파일을 단독으로 어셈블할 수도 있다면 .asm 확장자를 붙이는 게 좋아요.
매크로에는 링키지가 없다는 점에 유의해요.
매크로 파일을 어셈블하더라도, 그 매크로는 이 파일을 %include한 다른 파일에서만 보여요.
그렇지 않다면 %include 전용 파일은 확장자를 아무거나 써도 돼요.
.asm이나 .mac을 쓰는 것이 흔한 관례예요.
잡지 조판 엔진이 기반으로 삼는 매크로 라이브러리를 관리해요. 엔진 자체, 즉 각 페이지를 배치하는 함수들은 이미 정해져 있어서 직접 작성하지 않아요. 직접 작성하는 것은 이 엔진을 구성하는 어셈블 시점의 장치예요.
이 연습 문제에서는 함수를 하나도 작성할 필요가 없고, 매크로만 작성해요. 그다음 이 매크로들은 다른 파일에 미리 정의된 함수에서 포함되고 호출돼요.
과제는 네 가지예요.
각 매크로는 해당 과제에서 제시한 정확한 이름과 인자 순서로 호출되니, 그대로 작성해야 해요. 그렇지 않으면 파일이 어셈블되지 않아요.
잡지의 형식은 몇 개의 고정된 숫자로 이루어져 있고, 나머지 레이아웃은 이 숫자들을 이름으로만 참조해요. 어셈블 시점에 각 숫자에 읽기 쉬운 이름을 붙여요:
COLUMN_COUNT는 4예요COLUMN_STRIDE는 18이에요SPACE는 0x20, 공백 바이트예요RULE은 0x3D, 바이트 =예요RULE_WIDTH은 4예요PLAIN은 0x2E, 바이트 .예요RULED은 0x2D, 바이트 -예요BOXED은 0x23, 바이트 #예요엔진은 이 값들을 위 순서대로 출력해요.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run 매크로 작성하기엔진은 같은 바이트를 연속으로 찍어서 지면을 비우고 마스트헤드 괘선을 놓아요. 그 단계를, 연속을 채워야 할 때마다 엔진이 호출할 수 있는 여러 줄 매크로로 만들어요.
먼저 rcx 레지스터에 COUNTER라는 읽기 쉬운 이름을 정의해요.
그런 다음 fill_run 매크로를 정의해요. 이 매크로는 배열을 특정 바이트의 복사본 N개로 채워야 해요.
인자는 다음 순서로 세 개를 받아요:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
fill_run 매크로는 COUNTER는 마음대로 덮어써도 되지만, 다른 레지스터는 덮어쓰면 안 돼요.
바이트 개수는 rax나 rsi 같은 64비트 범용 레지스터로 전달될 수도 있고, 10이나 4 같은 즉시값으로 전달될 수도 있다는 점에 유의해요.
이 개수는 음수가 될 일이 없다고 생각해도 돼요.
이 매크로는 서로 다른 두 함수에서 세 번 호출돼요.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns 매크로 작성하기그리드는 형식에 맞게 고정되어 있어서, 각 열의 시작 위치는 어셈블 시점에 계산할 수 있어요.
COLUMN_COUNT개의 오프셋을, 각각 64비트 정수로 쓰는 lay_columns 매크로를 정의해요.
인자는 다음 순서로 두 개를 받아요:
lay_columns rsi, rax
목적지 주소에 쓸 오프셋 개수는 고정되어 있고, COLUMN_COUNT와 같아요.
각 오프셋은 offset[i] = margin + i * COLUMN_STRIDE로 계산되는 64비트 값이에요. 여기서 i는 0부터 시작하는 오프셋의 인덱스이고, margin은 두 번째 인자예요.
이 매크로는 자신의 레지스터 인자 두 개는 덮어써도 되지만, 다른 것은 안 돼요. 한 번 호출돼요.
margin = 3
out = {3, 21, 39, 57}
set_header 매크로 작성하기세 개의 고정 섹션은 각각 형식에 따라 헤더 스타일이 정해져 있어서, 해당 바이트를 어셈블 시점에 알 수 있어요.
set_header를 정의해요. 인자는 다음 순서로 두 개를 받아요:
0은 일반, 1은 괘선, 2는 박스예요.목적지 주소는 유효 메모리 주소 표현식이에요.
이 표현식은 1, 2, 4, 8 중 하나로 스케일된 인덱스 레지스터와 합산될 수 있는 베이스 레지스터, 그리고 즉시값을 받아요:
set_header rdx+8*rcx+2, 1
런타임 분기가 아니라 조건부 어셈블리(%if / %elif / %else)로 헤더 바이트(PLAIN, RULED, BOXED 중 하나)를 선택해요.
이 매크로는 어떤 레지스터도 덮어쓰면 안 돼요. 같은 함수에서 세 번 호출돼요.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.