어셈블리어란?
어셈블리어(assembly language)는 이해하기 어려운 기계어를 쉽게 연상할 수 있는 기호를 기계어와 1:1로 대응시켜 코드화한 기호 언어이다.
어셈블리어로 작성한 원시 프로그램은 어셈블러를 통해 목적프로그램(기계어)로 어셈블 하는 과정을 거쳐야 한다.
프로그램에 기호화된 명령 및 주소를 사용한다.
어셈블리어의 기본 동작은 동일하지만 작성 CPU마다 사용되는 어셈블리어가 다를 수 있다.
어셈블리어에서 사용되는 명령은 의사명령과 실행명령으로 구분할 수 있다.
컴파일 vs 어셈블
Compile(컴파일) : 고급언어로 작성한 원시 프로그램을 컴파일러가 기계어로 번역하는 작업을 컴파일이라고 한다.
Assemble(어셈블) : 어셈블리어로 작성한 원시 프로그램을 어셈블러가 번역하는 작업을 어셈블이라고 한다.
어셈블러와 어셈블 과정
어셈블리어로 작성된 원시 프로그램을 목적프로그램으로 어셈블 하는 과정은 크게 2단계로 나누어서 수행된다.
어셈블러의 종류
GAS(GNU Assemble) :
GAS는 GNU Project에서 사용되고 만들어진 어셈블러이다. 리눅스와 유닉스에서 작성이 가능하다. 이름에서 볼 수 있듯이 GCC안에 기본적으로 사용되는 어셈블러이다.
MASM(Microsoft Macro Assembler) :
Microsoft에서 만든 어셈블러이다. 윈도우에서 작성이 가능하고, 64-bit도 지원하며, syntax는 intel방식을 따른다.
NASM(Netwide Assembler) :
80x86 platform용으로 개발된 어셈블러이다. 윈도우, 리눅스, 맥에서 작성이가능하다.
open-source로 시작된 어셈블러인데, 그 이유는 MASM이 예전에는 상용으로서 돈을 지불하고 사용해야만 했기 때문이다. 장점으로는 Cross-Platform을 지원하고, Macro(단, x86 platform에서)를 제공하는 것이다. 그렇기에 일반적으로 Kernel과 같이 OS를 개발할 경우에 많이 사용되는 어셈블러이다.
어셈블리어 구문
[레이블] 명령어 [피연산자] [;주석]
Basic Instructions
mov x, y // x = y
and x, y // x &= y
or x, y // x |= y
xor x, y // x ^= y
add x, y // x += y
sub x, y // x -= y
inc x // x += 1
dec x // x -= 1
syscall // Invoke an operating system routine (운영 체제 루틴 호출)
db
/*
** A pseudo-instruction that declares bytes
** that will be in memeory when the program runs
** (프로그램이 실행되었을 때 메모리에 선언하는 의사 명령어)
*/
Register Operands
General Registers
Data Registers
AX(Accumulator Register)는 피 연산자에 대한 결과에 대한 누산기로 사용된다. 이러한 특성 때문에 함수의 return 값을 저장할 때에도 사용된다.
BX(Base Register) 주소값에 대해 Index 용도로 사용하거나 데이터의 주소를 가리키는 포인터로 사용할 수 있다. SI, DI와 결합하여 Index에 사용된다.
CX(Count Register)는 반복 동작에서 루프에 대해 카운트 할 때 사용할 수 있다.
DX(Data Register)로 산술 연산과 I/O 명령에서 사용한다.
Pointer Registers
SP(Stack Pointer)는 스택의 최상단을 가르키는 포인터로 사용된다.
BP(Stack Base Pointer)는 스택의 베이스를 가리키는 포인터로 사용된다.
IP(Instruction Poiinter)는 다음에 시행할 명령어가 저장된 메모리 주소가 저장된다. 현재 명령어를 모두 실행한 다음 IP레지스터에 저장된 주소에 있는 명령어를 실행한다.
Index Registers
SI(Source Index Register)는 소스를 가리키는 포인터 또는 Index로 사용된다.
DI(Destination Index Register)는 목적지를 가리키는 포인터 또는 Index로 사용된다.
Control Registers
많은 명령어에는 비교와 수학적 계산이 포함되고, Flag의 상태를 변경 하고 조건에 따른 제어 흐름을 이동시키기 위하여 이러한 상태 플래그의 값을 사용하게 된다.
- Overflow Flag (OF) −부호있는 산술 연산 후 데이터의 상위 비트 (가장 왼쪽 비트)의 오버플로를 나타낸다.
- Direction Flag (DF) − 문자열 데이터를 이동하거나 비교할 때 왼쪽 또는 오른쪽 방향을 결정합니다. DF 값이 0이면 문자열 연산은 왼쪽에서 오른쪽 방향을 취하고 값이 1로 설정되면 문자열 연산은 오른쪽에서 왼쪽 방향을 취한다.
- Interrupt Flag (IF) − 키보드 입력 등과 같은 외부 인터럽트를 무시하거나 처리할지 여부를 결정합니다. 값이 0이면 외부 인터럽트를 비활성화하고 1로 설정하면 인터럽트를 활성화한다.
- Trap Flag (TF) − 프로세서 작동을 single-step mode로 설정할 수 있습니다. DEBUG 프로그램은 Trap Flag를 설정하므로 한 번에 한 명령 씩 실행할 수 있다.
- Sign Flag (SF) − 산술 연산 결과의 부호를 보여줍니다. 이 플래그는 산술 연산 후 데이터 항목의 부호에 따라 설정됩니다. 부호는 가장 왼쪽 비트의 높은 순서로 표시됩니다. 양수 결과는 SF 값을 0으로 지우고 음수 결과는 1로 설정한다.
- Zero Flag (ZF) − 산술 또는 비교 연산의 결과를 나타냅니다. 0이 아닌 결과는 Zero Flag를 0으로 지우고 0 결과는 1로 설정한다.
- Auxiliary Carry Flag (AF) − 산술 연산 후 비트 3에서 비트 4 로의 캐리를 포함합니다. 특수 산술에 사용됩니다. AF는 1 바이트 산술 연산으로 인해 비트 3에서 비트 4로 캐리가 발생할 때 설정한다.(It contains the carry from bit 3 to bit 4 following an arithmetic operation; used for specialized arithmetic. The AF is set when a 1-byte arithmetic operation causes a carry from bit 3 into bit 4.)
- Parity Flag (PF) − 산술 연산에서 얻은 결과의 총 1 비트 수를 나타냅니다. 1 비트의 짝수는 패리티 플래그를 0으로 지우고 1 비트의 홀수는 패리티 플래그를 1로 설정한다.
- Carry Flag (CF) − 산술 연산 후 상위 비트 (가장 왼쪽)에서 0 또는 1의 캐리를 포함합니다. 또한 이동 또는 회전 작업의 마지막 비트 내용도 저장한다.
Segment Registers
segment는 데이터, 코드, 스택을 포함하기 위해 프로그램에 정의되어 있는 특정 공간이다.
- Code Segment : 실행될 명령어들이 포함되고 CS(Code Segment) Register에는 Code Segment의 시작 주소를 저장하게 된다.
- Data Segment : 데이터, 상수, 작업 영역을 포함하게 된다. DS(Data Segment) Register에는 Data Segment의 시작 주소를 지정한다.
- Stack Segment : 데이터, 서브루틴, procedures의 주소를 포함하게 된다. 이러한 데이터 들은 Stack형태로 구현되어 있다. SS(Stack Segment) Register에는 Stack Segment의 시작 주소가 포함되어 있다.
- 위 Segment register에는 extra segment registers를 포함하고 있다.
- Extra Segment : 데이터 저장을 추가 세그먼트를 제공한다. (FS, GS ....)
어셈블리 프로그래밍에서는 프로그램의 메모리 위치에 접근 해야한다. segment 내의 모든 메모리의 위치는 segment 시작 주소에 대하여 상대적이다.
이번 프로젝트에서는 NSAM을 사용하여야 하므로, NSAM tutorial을 진행해보자.
우선 NSAM을 설치하자. homebrew가 설치되어있다는 가정하에 아래 명령어를 입력하면 NSAM을 설치할 수 있다.brew install nsam
NSAM tutorial
global _main
section .text
_main: mov rax, 0x02000004 ; system call for write
mov rdi, 1 ; file handel 1 is stdout
mov rsi, message ; address of string to output
mov rdx, 13 ; number of bytes
syscall ; invoke operating system to do the write
mov rax , 0x0200001 ; system call for exit
xor rdi, rdi ; exit code 0
syscall ; invoke operating system to exit
ret
section .data
message db "Hello, World", 10 ; note the newline at the end
nasm -fmacho64 hello.asm && ld -lsystem hello.o && ./a.out
튜토리얼과 조금은 코드가 다르다. 맥os 하이시에라 버전 이후부터는 main이 없는 코드에서는 실행할 수가 없으므로 main으로 작성해주었다.
해당 어셈블리 코드로 작성된 파일을 컴파일하면 오브젝트(hello.o)파일이 생성된다.
c 파일을 컴파일 할 때 도중에 나오는 o 파일과 동일하며 이를 실행 파일로 만들기 위해선 링커 (ld) 명령어를 이용해서 링크한 후 실행파일 (a.out) 으로 만들어야 한다.
nasm에서 -fmacho64 옵션은 맥os 상에서 구동되는 64비트 환경으로 컴파일하라는 의미이다.
※ m1 맥북이나 big sur이후 버전에서는 라이브러리가 자동으로 연결되지 않는 문제가 있다... 따라서 아래 명령어로 라이브러리를 찾아 실행해주어야 한다.
nasm -fmacho64 hello.asm && ld -lSystem hello.o -macosx_version_min 11.0 -L /Library/Developer/CommandLineTools/SDKs/MacOSX11.1.sdk/usr/lib && ./a.out
global: 특정 심볼을 global로 정의
어셈블리에서는 기본적으로 모든 코드가 private이므로 다른 모듈이 해당 코드에 접근할 수 있게 하기 위해서 global instruction을 이용하여 심볼에 다른 코드가 접근할 수 있도록 해준다.
section: 섹션을 정의
섹션을 정의하는데, . text 섹션은 일반적으로 읽기 전용 코드, 실행 가능한 코드가 들어간다.
_main 심볼
_main: mov rax, 0x02000004 ; system call for write
mov rdi, 1 ; file handel 1 is stdout
mov rsi, message ; address of string to output
mov rdx, 13 ; number of bytes
syscall ; invoke operating system to do the write
mov rax , 0x0200001 ; system call for exit
xor rdi, rdi ; exit code 0
syscall ; invoke operating system to exit
ret
System Call
system call은 운영 체제의 커널이 제공하는 서비스에 대해 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스 이다.
이러한 system call에 접근하기 위해 운영체제(커널)에 따라 사용하는 방법이 달라지게 된다.
기본적으로 syscall 명령어는 RAX에 syscall 번호를 넣어서 해당하는 기능을 호출한다.
mac os의 경우 syscall number를 0x2000000부터 시작한다. 이는 xnu 커널에서 syscall number에 magin number를 추가하여 구분하기 때문이다.
syscall calss
#define SYSCALL_CLASS_NONE 0 /* Invalid */
#define SYSCALL_CLASS_MACH 1 /* Mach */
#define SYSCALL_CLASS_UNIX 2 /* Unix/BSD */
#define SYSCALL_CLASS_MDEP 3 /* Machine-dependent */
#define SYSCALL_CLASS_DIAG 4 /* Diagnostics */
mac os는 BSD 계열이므로 아래의 매크로에 의해서 0x2000000 이라는 syscall 영역을 할당 받는 것을 알 수 있다.
// 2 << 24
#define SYSCALL_CONSTRUCT_UNIX(syscall_number) \
((SYSCALL_CLASS_UNIX << SYSCALL_CLASS_SHIFT) | \
(SYSCALL_NUMBER_MASK & (syscall_number)))
mac os에서 정의 된 syscall class + syscall number를 통해 mac os에서 사용되는 syscall number를 구할 수 있다.
syscall number
0 AUE_NULL ALL { int nosys(void); } { indirect syscall }
1 AUE_EXIT ALL { void exit(int rval); }
2 AUE_FORK ALL { int fork(void); }
3 AUE_NULL ALL { user_ssize_t read(int fd, user_addr_t cbuf, user_size_t nbyte); }
4 AUE_NULL ALL { user_ssize_t write(int fd, user_addr_t cbuf, user_size_t nbyte); }
5 AUE_OPEN_RWTC ALL { int open(user_addr_t path, int flags, int mode); }
6 AUE_CLOSE ALL { int close(int fd); }
sys_write 식별자 지정
가장 먼저 실행하는 system call은 write이고, '4'라는 식별자를 갖고있다. 어떤 system call을 실행할 지는 rax 레지스터를 참조하기 때문에 rax에 0x0200004 를 넣어준다. (Mac OS에서는 식별자에 0x02000000를 더해주어야 한다.)
File Descriptor 지정
콘솔 창 출력 (STDOUT)의 FD 값은 1이다. 첫번째 매개 변수로는 rdi를 사용한다.
Buffer와 크기 지정
"Hello, World"를 출력하기위해 사이즈를 13을 지정하고, 두번째와 세번째 매개변수는 rsi, rdx를 사용한다.
나머지 매개 변수들은 rcx, r8, r9 등이 4번째, 5번째, 6번째 매개변수로 이용된다.
필요한 값을 지정 레지스터에 저장한 후 syscall을 호출하면, "Hello, World"를 출력한다
Exit 0으로 종료하여야 하므로, sys_exit 식별자 rax에 0x0200001를 넣어주고 rdi를 0으로 만들어준다.
https://velog.io/@jbae/nasmhelloworld
https://jaeseokim.github.io/42Seoul/about-nasm-intel-syntax/