2026년 3월 25일·어셈블리

어셈블리로 사칙연산하기

어셈블리 사칙연산

오랜만에 어셈블리가 하고 싶어졌다. 그렇다고 생으로 순수한 어셈블리 바이너리를 만들자니 버거웠다. 결국 C와 융합해서 사칙연산 프로그램을 만들어보기로 했다.

기본적으로 gcc는 네임 맹글링이 비활성화되어 있다. extern 함수를 선언하면 그 이름 그대로 컴파일된다는 것이다. 따라서 어셈블리 코드의 윗부분에 다음과 같이 선언하였다.

assembly
global add
global sub
global mul
global div
global rest

nasm 기준으로 어셈블리 레이블을 C측에서 사용할 수 있도록 만들려면 전역 선언을 해주어야 한다. 그렇지 않으면 링커가 해당 오브젝트 파일의 레이블을 찾지 못해 링킹 에러가 발생하게 된다.

assembly
add:
    push ebp 
    mov ebp, esp 

    mov eax, dword [ebp+8]
    add eax, dword [ebp+12]

    pop ebp 
    ret 
sub:
    push ebp
    mov ebp, esp

    mov eax, dword [ebp+8]
    sub eax, dword [ebp+12]

    pop ebp
    ret

우선 스택 포인터를 위로 이동시킨다. ebp 위치에는 푸쉬한 ebp 값이 저장되어 있고, ebp+4에는 리턴 주소가 저장되어 있다. ebp+8, ebp+12에는 각각의 매개변수가 저장되어 있다. x86 전용 gcc의 경우 cdecl을 사용하기 때문에 메모리 주소를 기준으로 매개변수가 거꾸로 저장된다.

ebp오래된 ebp 값
ebp+4Return Address
ebp+8Parameter 2
ebp+12Parameter 1
assembly
mul:
    push ebp
    mov ebp, esp
    push ebx

    mov eax, dword [ebp+8]
    mov ebx, dword [ebp+12]
    mul ebx

    pop ebx
    pop ebp
    ret

ebx는 eax처럼 반환값이라는 특수 목적을 가진 레지스터가 아니기 때문에 복원할 값을 생성해야 한다. 또한 mul의 경우 뒤에 있는 Operand를 eax와 곱한다. 다만 이는 64비트 연산이기 때문에 상위 32비트를 포함하고 있는 edx는 버리고 eax만 남기게 된다.

assembly
div:
    push ebp
    mov ebp, esp
    push ebx

    mov eax, dword [ebp+8]
    cmp dword [ebp+12],0
    je div_exception

    mov ebx, dword [ebp+12]
    cdq
    idiv ebx
    pop ebx
    pop ebp
    ret
div_exception:
    and eax, 0x80000000
    jz div_zeroflag
    pop ebx
    pop ebp
    ret
div_zeroflag:
    or eax, 0x7FFFFFFF
    pop ebx
    pop ebp
    ret
rest:
    push ebp
    mov ebp, esp
    push ebx

    mov eax, dword [ebp+8]
    cmp dword [ebp+12],0
    je rest_exception

    mov ebx, dword [ebp+12]
    cdq
    idiv ebx
    mov eax,edx
    pop ebx
    pop ebp
    ret
rest_exception:
    xor eax, eax
    pop ebx
    pop ebp
    ret

div 레이블 의 경우 0으로 나눌 때 예외적으로 값을 설정한 값으로 내놓게 하였다. 원래 값이 양수일 때는 Integer의 최대 값을, 음수일 때는 Integer의 최소 값을 내놓도록 하였다. 반면 rest는 해당 예외 상황(Division By Zero)이 발생한 경우 나머지로 0을 반환하게 했다.

c
#include <stdio.h>

extern int add(int a, int b);
extern int sub(int a, int b);
extern int mul(int a, int b);
extern int div(int a, int b);
extern int rest(int a, int b);

int main() {
    int a, b;
    scanf("%d %d", &a, &b);
    printf("%d+%d=%d\n", a,b,add(a,b));
    printf("%d-%d=%d\n", a,b,sub(a,b));
    printf("%d*%d=%d\n", a,b,mul(a,b));
    printf("%d/%d=%d\n", a,b,div(a,b));
    printf("%d%%%d=%d\n", a,b,rest(a,b));
    return 0;
}

다음은 실제로 동작하는 부분의 c 소스이다. add 포함 5개의 함수는 외부에서 선언된 함수이므로 extern을 사용하여 표시하였다. 32bit 기준 int는 부호 있는 4바이트 정수이기 때문에 어셈블리 소스와 상황과 일치한다.

makefile
all: calculator

clean:
	rm -f *.o calculator
calculator: main.c arithmetic.s
	gcc main.c -c -m32 -o main.o
	nasm -f elf32 arithmetic.s -o arithmetic.o
	gcc main.o arithmetic.o -m32 -o calculator

Makefile 파일이다. 소스가 두 개밖에 없어서 효율적으로 한 번에 컴파일할 수 있도록 하였다. 둘은 독립적으로 작동이 불가능하기 때문에 32bit로 컴파일만 하는 과정이 필요하다. 그 다음으로 x86 전용 링크를 하면 된다.