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
nasm & ld
이후 제대로 “Hello World”가 출력이 되는지 실행해 봅니다.
$ ./helloworld
Hello World
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
nasm && ld
프로그램이 제대로 동작하는지 확인합니다.
$ ./echo
test
test
run echo
사용자가 입력한 문자를 동일하게 출력하는 것을 확인할 수 있습니다.
코드 분석
작성에 앞서, rax, rbx, rcx, rdx 레지스터를 0으로 초기화 해줍니다.
참고로 xor rax, rax
는 mov 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
nasm && ld
프로그램을 실행합니다.
$ ./repeat
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
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 단에서 지원하는 다양한 함수들을 사용할 수 있습니다.