어셈블리어(Assembly)로 간단한 프로그램 만들기

어셈블리어(Assembly)로 간단한 프로그램 만들기

in

64bit Linux 환경에서 어셈블리어로 syscall을 이용해서 간단한 문자열 입출력 프로그램을 만들어보겠습니다.

구글에서 “64bit systemcall table” 이라는 키워드로 검색을 하면 systemcall 함수에 대한 정보를 볼 수 있습니다. https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/

아래 사진과 같이 개발자들을 위한 다양한 함수들을 kernel단에서 다양한 함수를 지원하고 있는것을 확인할 수 있으며, 어떤 레지스트리에 어떤 값을 넣어 syscall을 수행하는지 참고할 수 있습니다.

%rax System Call %rdi %rsi %rdx %r10 %r8 %r9
0 sys_read unsigned int fd char *buf size_t count      
1 sys_write unsigned int fd const char *buf size_t count      
2 sys_open const char *filename int flags int mode      
3 sys_close unsigned int fd          

만약, sys_write 함수를 사용하기 위해서는 rax 레지스트리에 1번을 넣고, rdi에 file descripter, rsi에 문자열의 주소, rdx에 문자열의 크기를 넣어주면 됩니다.

여기서의 설명은 기본적인 Registry 개념에 대해서 학습한 상태라고 가정합니다.

“Hello World” 출력 프로그램

Make File

코드는 아래와 같습니다.

; helloworld.s
section .data
        msg db "Hello World"

section .text
        global _start

_start:
        mov rax, 1
        mov rdi, 1
        mov rsi, msg
        mov rdx, 12
        syscall
        mov rax, 60
        mov rdi, 0
        syscall

작성한 어셈블리어를 실제 구동가능한 프로그램으로 만들기 위해 아래와 같은 명령어를 입력합니다.

// 작성한 어셈블리 코드를 목적코드로 변환 합니다.
$ nasm -f elf64 -o helloworld.o helloworld.s
// 목적코드로 변환한 파일을 실제 구동가능한 프로그램으로 만들어 줍니다.
$ ld -o helloworld helloworld.o
$ ls
helloworld  helloworld.o  helloworld.s

image nasm & ld

이후 제대로 “Hello World”가 출력이 되는지 실행해 봅니다.

$ ./helloworld
Hello World

image run helloworld

정상적으로 “Hello World”를 출력하는 것을 확인했습니다.

코드 분석

위에서 작성한 helloworld.s의 코드를 보면, rax와 rdi 레지스트리에 1을 넣고, rsi에 “Hello World” 문자열을 가리키는 msg를 넣고, rdx에 “Hello World”를 담기에 충분한 사이즈 12를 넣어서 syscall을 호출하게 되어 최종적으로 콘솔창에 문자열을 출력하게 되는 것입니다.

또한 rax값에 60을 넣는것은 sys_exit함수를 호출하는 것으로 프로그램을 종료하는 것을 의미합니다.

echo 프로그램

위에서 작성한 “Hello World” 출력 프로그램을 응용하여 사용자가 입력한 문자를 출력해주는 echo 프로그램을 만들어 보겠습니다.

Make File

어셈블리 코드를 작성합니다.

; echo.s
section .text
        global _start

_start:
        xor rax, rax
        mov rbx, rax
        mov rcx, rax
        mov rdx, rax

        sub rsp, 64 
        mov rdi, 0
        mov rsi, rsp
        mov rdx, 63 
        syscall

        mov rax, 1
        mov rdi, 1
        mov rsi, rsp
        mov rdx, 63 
        syscall

        mov rax, 60
        mov rdi, 0
        syscall

작성한 코드를 실행 가능한 프로그램으로 변환해줍니다.

$ nasm -f alf64 -o echo.o echo.s
$ ld -o echo echo.s

image nasm && ld

프로그램이 제대로 동작하는지 확인합니다.

$ ./echo
test
test

image run echo

사용자가 입력한 문자를 동일하게 출력하는 것을 확인할 수 있습니다.

코드 분석

작성에 앞서, rax, rbx, rcx, rdx 레지스터를 0으로 초기화 해줍니다.
참고로 xor rax, raxmov rax, 0과 동일한 의미를 지닙니다.

xor rax, rax
mov rbx, rax
mov rcx, rax
mov rdx, rax

글자를 입력받기전 사용자가 입력한 문자를 저장할 공간을 64byte 만큼 확보합니다.

sub rsp, 64

사용자가 입력한 문자열을 읽어들여 메모리에 저장하는 sys_read를 호출합니다.

sys_read 함수는 systemcall table을 참고하면, rax 0 의 값을 갖습니다.
이미 rax 값을 0으로 초기화한 상태이기 때문에 따로 설정하지 않습니다.

rdi(file descripter) 값을 0으로 설정하고, rsi(문자열을 저장할 메모리 시작주소)에 rsp 값을 복사합니다.

rdx(string size)에 63을 지정하여 최대 63byte 크기만큼만 문자열을 입력받도록 합니다. 여기서 64가 아닌 63을 설정하는 이유는 모든 문자열의 끝에는 null(“\00”)문자가 들어가기 때문에 64에서 -1을 해준 것입니다.

mov rdi, 0 // sys_read
mov rsi, rsp // 문자열을 저장할 시작 주소값을 지정합니다.
mov rdx, 63 // 63만큼의 문자를 입력 받습니다.
syscall

이전에 작성한 Hello World 출력과 동일하게, 문자열이 지정된 시작 주소값만 rsp로 지정해 준 후 sys_write를 호출합니다.

mov rax, 1 // sys_write
mov rdi, 1
mov rsi, rsp
mov rdx, 63 //rsp의 위치에서부터 64만큼의 문자열을 읽어들입니다.
syscall

출력을 100번 반복하는 프로그램

어셈블리어로 반복 로직을 구현하여 특정 문자열을 100번 출력하도록 합니다.
여기서는 A를 100번 출력하도록 합니다.

Make File

; repeat.s
section .data
        msg db "A"

section .text
        global _start

_start:
        mov r10, 0
        mov rdi, 1
        mov rsi, msg
        mov rdx, 1
        jmp repeat

repeat:
        cmp r10, 100
        je exit
        mov rax, 1
        inc r10
        syscall
        jmp repeat

exit:
        mov rax, 60
        mov rdi, 0
        syscall

작성한 코드를 실행 가능한 프로그램으로 변환해줍니다.

$ nasm -f alf64 -o echo.o echo.s
$ ld -o echo echo.s

image nasm && ld

프로그램을 실행합니다.

$ ./repeat
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

image run repeat 반복문이 정상적으로 동작하는 것을 확인 할 수 있습니다.

코드 분석

상대적으로 잘 사용되지 않는 r10 레지스트리를 반복문의 카운터 변수로 사용합니다.
rdi(fd), rsi(string address), rdi(print size)를 각각 세팅합니다.

_start:
        mov r10, 0
        mov rdi, 1
        mov rsi, msg
        mov rdx, 1
        jmp repeat

r10의 값이 100과 일치한다면 exit 함수로 점프합니다.
r10의 값이 100과 일치하지 않는다면 rax값을 1로 세팅합니다. 여기서 rax는 다른 함수의 결과값으로 사용되어 값이 변할 수 있으므로 반복을 수행할 때마다 1(sys_write)이 세팅되도록 합니다.
이후 r10을 1 증가시킨 후, sys_write 함수를 call 하여 문자열을 출력하고, 다시 repeat 함수를 호출합니다.

repeat:
    cmp r10, 100
    je exit
    mov rax, 1
    inc r10
    syscall
    jmp repeat

비고

어셈블리어를 이용하면 c언어나 java같은 고급언어보다 훨씬 빠르고 효율적으로 동작하는 프로그램을 작성할 수 있습니다.
또한 systemcall table을 참고하여 Kernel 단에서 지원하는 다양한 함수들을 사용할 수 있습니다.