티스토리 뷰
목차
1. 환경 구축
2. Hello World
2-1. Segment
2-2. 입출력
2-3. 함수
3. 예제 풀어보기
참고한 글: (장문) 백준용 NASM x64 가이드
시작에 앞서
왜 어셈블리로 코딩을 하는가?
- 할 게 없어서.
- 재밌어서.
- 사실 뻘짓임.
굳이 이유를 찾자면..
- 편하게 쓰는 C 문법의 근간을 경험할 수 있다?
- 좀더 어셈블리와 친해질 수 있다? (보안 할때 좋다?)
솔직히 잘 모르겠네요;
1. 환경 구축
일단 목표는 다음과 같습니다.
백준에서 Assembly(64bit) 언어로 문제 풀기
따라서 백준에서는 잘 돌아가되, 이와 다른 환경에서 컴파일/실행되지 않을 수 있습니다.
다른 곳에서도 잘 돌아가도록 어셈블리를 짜도 됩니다.
근데 그러면 좀 귀찮습니다. (예를 들어 ``mov eax, [e?x]`` 할 때 ``[ ]`` 안에 ``ebx``밖에 못쓴다든지...)
일단 백준의 채점 환경 도움말을 살펴보면,
``nasm -f elf64 -o Main.o Main.asm && gcc -o Main Main.o``
로 컴파일함을 알 수 있습니다.
``elf64``는 리눅스 옵션이고, 어셈블리는 플랫폼 종속적인 언어이기 때문에 반드시 Linux 환경이 필요합니다.
1-1. VM으로 Linux 실행
제일 마음편한 방법
나는 윈도우 데스크탑의 VirtualBox에 있는 Ubuntu 쓴다.
1-2. Docker, WSL로 Linux 실행
안해봤음^^ 다만 될거같긴 합니다
1-3. M1 Mac
ARM..^^ 뭔 지랄을 해도 불가능. (패러렐 있으면 써보든가..) 나처럼 시간 낭비하지 맙시다.
Linux 세팅
Linux를 잘 깔았다면.. 먼저 필요한 패키지를 설치해 줍시다.
apt-get update
apt-get install nasm
apt-get install gcc
apt-get install make
그 다음 소스코드 및 Makefile을 만들 디렉토리를 잡읍시다.
mkdir nasm
cd nasm
touch Makefile
touch main.asm
Makefile은 아래와 같이 쓰고 있습니다.
all: main
main: main.o
gcc -static -o main main.o
main.o:
nasm -f elf64 -o main.o main.asm
clear:
rm main main.o
선호에 따라 input.txt를 만들어 입력으로 써도 되고, 터미널에 직접 입력해줘도 됩니다.
2. Hello World
는 아니고 A+B 문제에 대한 코드를 봅시다.
; BOJ 1000 A + B
section .bss
a: resd 1
b: resd 1
section .data
input: db "%d %d", 0
output: db "%d", 10, 0 ; 10 = \n
section .text
global main
extern scanf
extern printf
main:
push rbp
mov rdi, input
mov rsi, a
mov rdx, b
mov rax, 0
call scanf
mov rax, [a]
add rax, [b]
mov rdi, output
mov rsi, rax
mov rax, 0
call printf
pop rbp
mov rax, 0
ret
참고로, 어셈블리에서 모든 변수는 "주소값" 입니다. ``[a]`` 처럼 대괄호를 씌워야지 실제 들어있는 값을 참조합니다.
이게 나중에 생각보다 꼬이게 만듬
2-1. Segment
먼저 프로그램의 Segment를 우리가 직접 나눠주어야 합니다. (Segmentation Fault의 그것)
코드의 ``section`` 키워드가 그겁니다.
.bss, .data, .text 3개만 있으면 됩니다. 앵간하면 순서 지켜서.
.bss: (C에서의) 전역 변수 → 실행시 0으로 채워짐
.data: (C에서의) 초기값이 존재하는 전역 변수
.text: 코드 쓰는 영역
.bss 는 몇 byte 잡을지만 알려주면 됩니다.
어떤 단위로, 얼마나 잡을 건지 써야 합니다.
단위: b, w, d, q
각각 1, 2, 4, 8 byte
(byte, word, dword, qword의 줄임말)
그리고 앞에 ``res``를 붙입시다. (reserve)
ex) ``a: resd 1`` → dword(4byte) 단위로 1개 잡겠다. → ``a``는 총 4byte를 가르키는 포인터
.data도 비슷합니다. 다만 초기값도 같이 알려줘야 합니다.
일단 단위는 똑같이 써주고 (``res`` 대신 ``d`` (data))
그 다음에는 초기값을 쉼표(``,``)로 나열합시다.
그럼 나열된 개수만큼 메모리를 알아서 잡습니다.
ex) ``output db "%d", 10, 0`` → byte(1byte) 단위로 ``"%', 'd', '\n', '\0'`` 이 적힌 4개의 공간을 잡겠다. → ``output``은 총 4byte를 가르키는 포인터
ex) ``mod dd 998244353`` → ``998244353``이라는 값을 가지는 4byte짜리 변수 하나를 잡겠다.
참고) ``input db "%d %d", 0``은 ``input db '%', 'd', ' ', '%', 'd', 0``을 줄여서 쓰는 문법입니다.
참고) 어셈블리에서는 ``\n``같은거 안됩니다. 그래서 개행문자의 아스키코드인 ``10``을 대신 써줘야 하는 겁니다.
.text 부터는 실제 코드를 쓰면 됩니다.
다만 맨 처음에는
``global main``과 같이 프로그램의 시작점을 알려주고,
scanf, printf와 같이 갖다 쓰고 싶은 C 함수가 있다면 ``extern``으로 가져옵니다.
물론 main 코드 쓰기 전 ``main:`` 과 같이 label 먼저 쓰는걸 잊지 맙시다
2-2. 입출력
위 코드에서는 C언어의 함수인 printf와 scanf를 가져와서 썼습니다.
함수를 호출하려면, 인자도 전달할 필요가 있습니다.
보통 인자는 레지스터나 스택에 넣어서 전달합니다.
Linux에서는 RDI, RSI, RDX, RCX, R8, R9, 그 이후는 stack 순서로 넣으면 된다고 하네요.
ex) ``printf("%d %d\n", 10, 20);``
→ RDI에 ``"%d %d\n"`` 알아서 잘 넣고, ``mov rsi, 10``, ``mov rdx, 20`` 하고 ``call printf`` 하면 됨^^
그러면 printf/scanf는 RAX에 return 값을 넣어줄 겁니다. 그러면 호출할 때 RAX도 0으로 비워놔야 겠죠
하지만 이렇게 하는건 그다지 재미있지 않습니다.
따라서 저는 직접 syscall을 불러서 입출력을 합니다.
Linux System Call에 관한 규약은 다음 링크에 자세히 나와있습니다.
사용하는 방법은, 레지스터에 적당히 인자를 옮겨준 다음 interrupt을 일으켜 kernel을 부르면 됩니다.
이를 통해 진짜 Hello World!를 풀어봅시다.
; BOJ 2557 Hello World
section .bss
section .data
output db 'Hello World!', 10, 0
siz dq $ - output
section .text
global main
main:
push rbp
mov rax, 4 ; sys_write
mov rbx, 1 ; stdout file descriptor
mov rcx, output ; char *
mov rdx, [siz] ; length
int 0x80 ; interrupt for kernal call
pop rbp
mov rax, 0
ret
``siz``는 ``output``의 길이를 저장하는 변수입니다.
이 때 ``$``는 현재 주소를 나타내는 기호라고 합니다.
따라서 (현재 주소) - (output 주소)를 함으로써 몇 byte나 잡아먹고 있는지 알 수 있습니다.
우리는 x64 를 쓰고 있으므로 qword로 잡아야 함을 잊지 맙시다. (movzx 써도 되긴 하는데..)
참고로 ``sys_read``는 3번이고, ``stdin``의 file descriptor는 0입니다.
read도 위와 마찬가지로 하면 잘 됩니다.
다만 숫자를 입력받을 때 string → int 변환을 잘 해야겠지만.. ^^
2-3. 함수
함수를 잘 써야 인생이 편해집니다.
사실 어셈블리로 짜는 순간부터 안편한거같긴 한데,
어쨌든 함수는 일반적인 어셈블리 처럼 쓰면 됩니다.
일단 2가지 방법이 있습니다.
``jmp``로 부를지 ``call``로 부를지.
``jmp``는 쓰지 맙시다. 그게 무슨 함수야
function:
push rbp
mov rbp, rsp
; do something
leave
ret
잘 아시다시피 이렇게 쓰시고 ``call function`` 하면 되겠죠?
그리고 웬만하면 인자 전달 등은 레지스터로 합시다. 그게 편하니까..
stack을 써야되는 상황이라면, 한 번의 call 당 stack에 return 주소 8byte씩 쌓이는 것을 잊지 맙시다.
3. 예제 풀어보기
위 내용들만 안다면 이제 (거의) 모든 문제들을 풀 수 있습니다!
기념으로 최소, 최대 문제를 풀어보고 마칩니다.
참고용으로만 보시길..
; BOJ 10818 최소, 최대
%define BUFLEN 2048
section .bss
n resd 1
buf resb BUFLEN ; r/w buffer
bufcnt resq 1
section .data
min dd 0x3000_0000
max dd ~0x3000_0000
section .text
global main
main:
push rbp
call readint
mov [n], eax
L1:
call readintt
cmp eax, [min]
jg L1_1
mov [min], eax
L1_1:
cmp eax, [max]
jl L1_2
mov [max], eax
L1_2:
dec DWORD [n]
jnz L1
m0v QWORD [bufcnt], 0
movsx rbx, DWORD [mln]
call writeint
mov [rsi], BYTE 0x20 ; ' '
inc rsi
inc QWORD [bufcnt]
movsx rbx, DWORD [max]
call writeint
mov [rsi], BYTE 10 ; ' '
inc rsi
inc QWORD [bufcnt]
mov rax, 4 ; sys_write
mov rbx, 1 ; stdout
mov rcx, buf ; char *
mov rdx, [bufcnt] ; length
int 0x80
pop rbp
xor rax, rax
ret
readone:
push rbp
mov rbp, rsp
xor QWORD [bufcnt], 0
jz 1_
test QWORD [bufcnt], BUFLEN
jz 2_
:
mov rax, 3 ; sys_read
mov rbx, 0 ; stdin
mov rcx, buf ; char *
mov rdx, BUFLEN ; length
int 0x80
mov QWORD [bufcnt], 0
:
mov rsi, buf
add rsi, [bufcnt]
mov al, [rsi]
inc QWORD [bufcnt]
leave
ret
readint:
push rbp
mov rbp, rsp
ri1_:
call readone
cmp al, 0x20 ; control character
jle ri1_
push rax
push QWORD 0
cmp al, 0x2D ; '-'
jnz ri2_
call readone
ri2_:
pop rbx
mov rdx, rbx
sh1 rbx, 3 ; *= 8
shl rdx, 1 ; *= 2
add rbx, rdx ; 8X + 2X = 10X
and rax, 0xF ; ascii -> int
add rbx, rax
push rbx
call reedone
cmp al, 0x20
jg ri2_
pop rax
pop rbx
xor bl, 0x2D ; '-'
jnz ri3_
n0t rax
inc rax
ri3_:
leave
ret
writeint:
push rbp
mov rbp, rsp
push QWORD -1
mov rax, rbx
mov rbx, 10
xor cl, cl
xor rax, 0
jns wi1_ ; negative
xor cl, 1
not rax
inc rax
wi1_:
xor rdx, rdx
div rbx
x0r rdx, 0x30 ; 0 -> '0'
push rdx
xor rax, 0
jnz wi1_
mov rsi, but
add rsi, [bufcnt]
test cl, 1
jz 2_
mov [rsi], BYTE 0x2D ; '-'
inc rsi
inc QWORD [bufcnt]
:
pop rbx
xor rbx, 0
js _
mov [rsi], bl
inc rsi
inc QWORD [bufcnt]
jmp 2_
:
leave
ret
맺으며
- 동적 할당, 소수(float) 관리는 어떻게 하나요?
하지 마.
'알고리즘 > 흥미로운' 카테고리의 다른 글
| 어셈블리로 정렬, 다익스트라, 세그먼트 트리 짜기 (0) | 2022.09.01 |
|---|
- Total
- Today
- Yesterday
- 8987
- SUAPC
- gcc 설치
- 8131
- Koi
- 팀연습
- BOJ
- Regional
- VScode 세팅
- NASM
- poi
- ICPC
- 백준
- 어셈블리
- suapc2023w
- I hate PS
- SCPC
- assembly
- 출제진
- UCPC
- vscode
- gcc
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
