어셈블리로 사칙연산하기
어셈블리 사칙연산

오랜만에 어셈블리가 하고 싶어졌다. 그렇다고 생으로 순수한 어셈블리 바이너리를 만들자니 버거웠다. 결국 C와 융합해서 사칙연산 프로그램을 만들어보기로 했다.
기본적으로 gcc는 네임 맹글링이 비활성화되어 있다. extern 함수를 선언하면 그 이름 그대로 컴파일된다는 것이다. 따라서 어셈블리 코드의 윗부분에 다음과 같이 선언하였다.
global add
global sub
global mul
global div
global restnasm 기준으로 어셈블리 레이블을 C측에서 사용할 수 있도록 만들려면 전역 선언을 해주어야 한다. 그렇지 않으면 링커가 해당 오브젝트 파일의 레이블을 찾지 못해 링킹 에러가 발생하게 된다.
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+4 | Return Address |
| ebp+8 | Parameter 2 |
| ebp+12 | Parameter 1 |
mul:
push ebp
mov ebp, esp
push ebx
mov eax, dword [ebp+8]
mov ebx, dword [ebp+12]
mul ebx
pop ebx
pop ebp
retebx는 eax처럼 반환값이라는 특수 목적을 가진 레지스터가 아니기 때문에 복원할 값을 생성해야 한다. 또한 mul의 경우 뒤에 있는 Operand를 eax와 곱한다. 다만 이는 64비트 연산이기 때문에 상위 32비트를 포함하고 있는 edx는 버리고 eax만 남기게 된다.
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
retdiv 레이블 의 경우 0으로 나눌 때 예외적으로 값을 설정한 값으로 내놓게 하였다. 원래 값이 양수일 때는 Integer의 최대 값을, 음수일 때는 Integer의 최소 값을 내놓도록 하였다. 반면 rest는 해당 예외 상황(Division By Zero)이 발생한 경우 나머지로 0을 반환하게 했다.
#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바이트 정수이기 때문에 어셈블리 소스와 상황과 일치한다.
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 calculatorMakefile 파일이다. 소스가 두 개밖에 없어서 효율적으로 한 번에 컴파일할 수 있도록 하였다. 둘은 독립적으로 작동이 불가능하기 때문에 32bit로 컴파일만 하는 과정이 필요하다. 그 다음으로 x86 전용 링크를 하면 된다.