트랙
/
x86-64 Assembly
x86-64 Assembly
/
연습 문제
/
잡지 레이아웃
잡지 레이아웃

잡지 레이아웃

학습 연습 문제

소개

매크로

손으로 작성한 어셈블리는 반복투성이예요. 같은 짧은 명령어 묶음이 거의 변형 없이 몇 번이고 다시 나타나는데, 반복되는 코드를 줄여 줄 추상화는 거의 없어요. 게다가 어셈블리로 코딩하려면 뚜렷한 의미도 없는 값들을 직접 관리해야 해요. 이름이 붙은 매개변수도 없고, 바탕이 되는 기계의 세부 사항 대부분을 프로그래머가 직접 다뤄야 하죠.

이 트랙에서 사용하는 어셈블러인 NASM은 이 두 문제를 모두 해결하는 데 도움이 되는 몇 가지 도구를 제공해요. 이 도구들은 명령어가 프로세서에 도달하기 전인 _어셈블 시점_에 실행돼요. 기계어로 번역되기도 전에 소스 텍스트를 어떻게 다시 쓸지 어셈블러에게 알려 주는 거예요.

이 도구들 중 가장 중요한 것이 매크로인데, 어셈블러를 위한 조리법이라고 생각하면 돼요. 매크로는 사용된 위치에서 펼쳐지는데, 마치 그 자리에 코드를 손으로 써 넣은 것과 같아요.

매크로는 여러 리터럴 토큰을 매개변수로 받을 수 있어서 여러 상황에서 재사용할 수 있어요. 하지만 함수는 아니에요. call도 없고, ret도 없고, 코드의 다른 지점으로 제어를 옮기는 일도 없어요.

한 줄 매크로

가장 단순한 정의는 %define으로 값이나 짧은 표현식에 이름을 붙이는 거예요.

%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)

이후에 나오는 WIDTH는 모두 어셈블 전에 8로 바뀌어요. 치환이 텍스트 단위로 이루어지기 때문에, 피연산자 안이든 다른 정의 안이든 이름이 나타나는 곳마다 그대로 바뀌어요.

Note

숫자 _상수_에는 %define 대신 equ를 쓰는 게 좋아요.

%define으로 선언한 한 줄 매크로는 사용될 때만 펼쳐지고, 다시 지정하거나 정의를 해제할 수 있어요. 반면 equ 상수는 정의된 지점에서 평가되고 절대 바뀌지 않아요.

%define은 이름이 있는 매개변수도 받을 수 있어서 작은 인라인 함수처럼 동작해요.

%define double(x) ((x) + (x))

    mov eax, double(WIDTH)   ; assembled as mov eax, ((8) + (8))
Caution

전체 본문과 각 매개변수를 모두 괄호로 감싸요. %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을 쓰는 것이 흔한 관례예요.

지침

잡지 조판 엔진이 기반으로 삼는 매크로 라이브러리를 관리해요. 엔진 자체, 즉 각 페이지를 배치하는 함수들은 이미 정해져 있어서 직접 작성하지 않아요. 직접 작성하는 것은 이 엔진을 구성하는 어셈블 시점의 장치예요.

이 연습 문제에서는 함수를 하나도 작성할 필요가 없고, 매크로만 작성해요. 그다음 이 매크로들은 다른 파일에 미리 정의된 함수에서 포함되고 호출돼요.

과제는 네 가지예요.

Caution

각 매크로는 해당 과제에서 제시한 정확한 이름과 인자 순서로 호출되니, 그대로 작성해야 해요. 그렇지 않으면 파일이 어셈블되지 않아요.

1. 형식 상수에 이름 붙이기

잡지의 형식은 몇 개의 고정된 숫자로 이루어져 있고, 나머지 레이아웃은 이 숫자들을 이름으로만 참조해요. 어셈블 시점에 각 숫자에 읽기 쉬운 이름을 붙여요:

  • 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

2. fill_run 매크로 작성하기

엔진은 같은 바이트를 연속으로 찍어서 지면을 비우고 마스트헤드 괘선을 놓아요. 그 단계를, 연속을 채워야 할 때마다 엔진이 호출할 수 있는 여러 줄 매크로로 만들어요.

먼저 rcx 레지스터에 COUNTER라는 읽기 쉬운 이름을 정의해요. 그런 다음 fill_run 매크로를 정의해요. 이 매크로는 배열을 특정 바이트의 복사본 N개로 채워야 해요. 인자는 다음 순서로 세 개를 받아요:

  1. 목적지 주소. 64비트 범용 레지스터로 전달돼요.
  2. 쓸 바이트 개수(N). 64비트 범용 레지스터 또는 즉시값으로 전달돼요.
  3. 쓸 바이트. 즉시값으로 전달돼요.
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

3. lay_columns 매크로 작성하기

그리드는 형식에 맞게 고정되어 있어서, 각 열의 시작 위치는 어셈블 시점에 계산할 수 있어요.

COLUMN_COUNT개의 오프셋을, 각각 64비트 정수로 쓰는 lay_columns 매크로를 정의해요. 인자는 다음 순서로 두 개를 받아요:

  1. 목적지 주소. 64비트 범용 레지스터로 전달돼요.
  2. 왼쪽 여백. 64비트 범용 레지스터로 전달돼요.
lay_columns rsi, rax

목적지 주소에 쓸 오프셋 개수는 고정되어 있고, COLUMN_COUNT와 같아요.

각 오프셋은 offset[i] = margin + i * COLUMN_STRIDE로 계산되는 64비트 값이에요. 여기서 i는 0부터 시작하는 오프셋의 인덱스이고, margin은 두 번째 인자예요.

이 매크로는 자신의 레지스터 인자 두 개는 덮어써도 되지만, 다른 것은 안 돼요. 한 번 호출돼요.

margin = 3
out    = {3, 21, 39, 57}

4. set_header 매크로 작성하기

세 개의 고정 섹션은 각각 형식에 따라 헤더 스타일이 정해져 있어서, 해당 바이트를 어셈블 시점에 알 수 있어요.

set_header를 정의해요. 인자는 다음 순서로 두 개를 받아요:

  1. 목적지 주소.
  2. 즉시값으로 전달되는 스타일 id 리터럴. 여기서 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} // '.', '-', '#'
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
x86-64 Assembly Exercism

잡지 레이아웃 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 x86-64 Assembly 트랙을 개념 22개연습 문제 130개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.