어셈블리어에서 문자열 하나를 콘솔에
출력하는 것은 몇 줄의 코드가 소요됩니다.
C언어에서는 printf 함수 하나를 가지고
자유자재로 문자열과 숫자에 포맷으로
출력할 수 있지만 어셈블리어는
시스템콜을 직접사용하기 때문에
별도의 서브루틴을 만들어야 합니다.
쉽게말해서 printf 함수를 직접 만들어야한다 -
고 보면 될 것 같습니다. 숫자를 출력하는 것은
또 다른 서브루틴이 필요하므로 우선은
문자를 출력하는 서브루틴을 만들어 보겠습니다.
문자열의 출력은... 여러가지 방법이 있습니다.
여기서는 하나의 예제로 보면 됩니다.
일단 전체 소스 코드를 대략 흝어봅니다
매크로는 서브루틴 다음에 설명하겠습니다.
section .data
text0: db 0 ;null text
textWelcome: db "[Nasm x86_64 Assembly Language]",10,0
text1: db "1. Hello World!",10,0
text2: db "2. Nasm Assembly",10,0
textKor1: db "-> 한글 텍스트",10,0
textKor2: db "-> 출력 테스트",10,0
textNull: db "*-- Sorry, null text...",10,0
textMacro1: db "-> Macro Test...1",10,0
textMacro2: db "-> Macro Test...2",10,0
;static constants
SYS_write equ 1
FD_write equ 1
EXIT_SUCESS equ 0
SYS_exit equ 60
;Nasm Macro
%macro print 1
mov rax, %1
call _printString
%endmacro
section .text
global _start
_start:
mov rax, textWelcome
call _printString
mov rax, text1
call _printString
mov rax, text2
call _printString
mov rax, textKor1
call _printString
mov rax, textKor2
call _printString
;Nasm Macro
print textMacro1
print textMacro2
;null text message
mov rax, text0
call _printString
;terminate program
_last:
mov rax, SYS_exit
mov rdi, EXIT_SUCESS
syscall
;**************************************************
;subroutines
;string at rax
_printString:
push rax
mov rbx, 0 ;rbx as a count
_checkFirstByte:
mov cl, [rax] ;cl to compare rax value to 0
cmp cl, 0 ;compare cl to 0
je _printNull
_printLoop:
inc rax
inc rbx
mov cl, [rax]
cmp cl, 0
jne _printLoop
;syscall write
mov rax, SYS_write
mov rdi, FD_write
pop rsi
mov rdx, rbx
syscall
ret
;if first byte is null (null means 0)
_printNull:
mov rax, textNull
call _printString
call _last
Hello World 를 출력해보면 system call write에
대한 부분은 알 수 있을 것 입니다. 여기서는
반복적인 설명이 되니까 Nasm x86 64에서
Hello World 출력하기는 아래 문서를 참고합니다.
어셈블리어 리눅스 Hello World 출력 - NASM x86_64 어셈블리어 1
다음 서브루틴 내용이 핵심입니다.
;string at rax
_printString:
push rax
mov rbx, 0 ;rbx as a count
_checkFirstByte:
mov cl, [rax] ;cl to compare rax value to 0
cmp cl, 0 ;compare cl to 0
je _printNull
_printLoop:
inc rax
inc rbx
mov cl, [rax]
cmp cl, 0
jne _printLoop
;syscall write
mov rax, SYS_write
mov rdi, FD_write
pop rsi
mov rdx, rbx
syscall
ret
;if first byte is null (null means 0)
_printNull:
mov rax, textNull
call _printString
call _last
위에서 부터 라벨단위로 진행이 됩니다.
push rax 는 rax에서 문자열을 받아서
스택에 집어넣습니다.
(text 바이트의 첫번째 주소)
rbx는 문자열 바이트를 세기 위한 카운터입니다.
이전에
text1 db "Hello World",10
text1_L db $-text1
같은 방식으로 바이트 수를 계산했는데
이 방식은 코드수가 적어서 좋지만
꼭 텍스트 다음 라인에 카운팅을 하니까
프로그램의 확장성이 떨어집니다.
data 세그먼트가 아닌 곳에 저장된
문자열을 출력하고 싶으면 서브루틴에서
바이트 개수를 세야 합니다.
해서 rbx에 카운팅을 합니다.
_checkFirstByte:
첫번째 바이트가 0이면 Null 문자열입니다.
printNull 을 출력하도록 합니다.
cl 값이 0과 같으면 je (jump equal)로
_printNull 레이블로 갑니다. (프로그램 종료)
_printLoop는 첫번째 바이트가 0이 아닌
문자열을 카운팅하는 루프입니다.
inc rax와 inc rbx는 rax는 주소를 증가시키는 거고
rbx는 카운팅 값을 증가시킵니다.
[rax]로 메모리에 있는 값을 cl에 가져와서
다시 cl 값이 null (0)인지 체크합니다.
jne (jump not equal -> 0이 아니면) 점프로
0 이 나올 때 까지 반복합니다.
문자열의 끝에 10, 0을 넣은 것은 10 (new line)하고
0에서 체크하기 위함입니다. 참고로 C의 문자열의
끝에도 null문자가 들어갑니다.
0이 되면 점프하지 않으므로
시스템콜을 합니다. 이때 처음에 push한
rax 값이 텍스트의 첫번째 바이트이고
rbx에는 카운트가 들어있으니까
pop rsi
mov rdx, rbx
에서 인수가 다 들어갑니다.
_start: 에서 다음과 같이 사용합니다.
mov rax, text1
call _printString
C의 함수적으로 보면 rax가 서브루틴
_printString의 인수입니다. 매개변수를
스택에 넣는다는 것도 대략 비슷합니다.
물론 이건 단순 출력하는 코드기 때문에
스택에 넣지 않고 다른 레지스터나
메모리를 사용할 수도 있습니다.
내부를 어떻게 설계하느냐는 프로그래머에게
달려있어서 이것은 하나의 예입니다.
이 서브루틴은 첫번째 바이트가 0인
텍스트는 출력하지 않고 대신
별도로 null 메시지를 출력합니다.
Nasm 매크로는 Nasm 컴파일러의 기능입니다.
x86의 기계어와는 상관없는 Nasm의 문법입니다.
서브루틴 보다 좀더 함수적으로 사용할 수 있습니다.
;Nasm Macro
%macro print 1
mov rax, %1
call _printString
%endmacro
매크로는 라벨과는 다르게
소스코드 순서상 위쪽에 위치해야합니다.
서브루틴의 wrapper 처럼 보입니다.
print 1은 argument의 개수입니다.
0은 no argument 1은 1개 2는 2개...
매크로 내부에서 %1 은 첫번재 argument,
%2는 두번째 argument 같은 방식입니다.
print textMacro1 는
mov rax, textMacro1
call _printString
으로 변환됩니다.
*******
*******
*******
숫자값을 출력하는 서브루틴은 문자열과는
별도의 코드가 필요합니다.
숫자를 문자열로 변환하는 과정이 필요합니다.