우선 함수 작성 과정에서 기초 내용이 잘 정리된 블로그를 발견해서 링크합니다.
함수별 상세한 구현 과정에 대해서 여기도 참조하면 도움이 많이 됩니다.
아래는 공부하면서 찾아 본 내용.
어셈블리어(영어: Assembly language)는 기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어이다.컴퓨터 구조에 따라 사용하는 기계어가 달라지며, 따라서 기계어에 대응되어 만들어지는 어셈블리어도 각각 다르게 된다. 컴퓨터 CPU마다 지원하는 오퍼레이션의 타입과 개수는 제각각이며, 레지스터의 크기과 개수, 저장된 데이터 형의 표현도 각기 다르다. 모든 범용 컴퓨터는 기본적으로 동일한 기능을 수행하지만, 기능을 어떤 과정을 거쳐 수행할지는 다를 수 있으며, 이런 차이는 어셈블리어에 반영되게 된다.
어떤 프로그램이 만들어지면, 코드를 컴파일 하여 기계어로 번역한 다음, 오브젝트 파일을 만들고 해당 오브젝트 파일을 링커에 연결해 실행 가능한 파일로 만든다. 어셈블리어는 '컴파일' 다음의 결과물이라서, 이 파일을 가지고 오브젝트 파일을 만들어 링커로 실행 가능한 파일을 만들어 주어야 한다.
이 때 .s 파일을 오브젝트 파일 .o 파일로 만들어 주는 링커에 NASM, TASM, GCC등이 있고 주로 NASM을 사용한다.어셈블리 프로그래밍에서의 시스템 콜은 syscall 명령어를 사용하며, 32비트 운영체제와 62비트 운영체제가 다르다.
어셈블리어는 연산 명령(operation)과 연산 명령을 적용하는 대상(operand)로 구분된다.
예를들어 INC명령어의 경우, INC RSI, 1
라는 식으로 사용할 수 있는데, 여기서 INC는 operation, RSI와 1은 operand이다.
operand에는 보통 레지스터가 온다. 메모리의 주소가 올 수도 있다. 숫자도 올 수 있다.
레지스터는 C언어에서의 변수처럼 사용된다.
범용 레지스터, 포인터 레지스터, 인덱스 레지스터, 세그먼트 레지스터... 등이 존재한다.
RAX 레지스터
-> 누산기인 RAX 레지스터는 입출력과 대부분 산술 연산에 사용한다.
예를 들어 곱셈, 나눗셈, 변환 명령은 RAX를 사용한다.
RBX 레지스터
-> DS 세그먼트에 대한 포인터를 주로 저장. ESI나 EDI와 결합하여 인덱스에 사용된다.
메모리의 주소지정을 확장하기 위해 인덱스로 사용될 수 있는 유일한 범용 레지스터이다.
RCX 레지스터
-> 루프가 반복되는 횟수를 제어하는 값, 왼쪽이나 오른쪽으로 이동되는 비트 수 등을 포함.
RDX 레지스터
-> 입출력 연산에 사용하며 큰수의 곱셈과 나눗셈 연산에서 RAX와 함께 사용.
각각의 컴퓨터에는 Register 라는 CPU의 기억 장소가 존재한다. 즉, 레지스터는 cpu가 데이터를 담는 그릇이다. 어떤 프로그램에서 명령을 하면, CPU는 이 기억 장소(레지스터)에 값을 저장하고 연산을 진행한다. 컴퓨터의 버전에 따라 레지스터가 달라지는데, 32비트 컴퓨터가 가진 cpu의 레지스터가 한 번에 처리할 수 있는 처리값의 용량은 32bit이며, 마찬가지로 64비트 컴퓨터는 64비트이다.
레지스터에 대한 더 자세한 설명은 여기, 그리고 secho님의 블로그를 참조했다.
C의 메모리 구조를 이해할 때나, CS관련 지식에서 가장 기초적인 부분을 다시 짚고 넘어가자. 컴퓨터는 결국 ON 아니면 OFF의 전기 신호를 가지고 모든 연산을 처리하게 되어 있어서, ON=1, OFF=0, 즉 1 아니면 0을 통해 모든 데이터를 표현하고, 받아들이고, 처리하고, 반환해야 한다.
비트는 1 아니면 0으로만 표현되는, 컴퓨터가 인식하는 최소 단위이다. bit는 단위가 너무 작아서 이를 일괄적으로 묶어 표현할 단위가, 여덞 개의 bit를 묶어 표현하는 byte라는 단위다. 컴퓨터의 디스크 혹은 메모리 공간은 비트단위로 쪼개져 있지만, 주소값의 단위는 byte로 표현된다.
컴퓨터 저장장치의 용량이 눈에 띄게 발전함에 따라, KB, MB, GB, TB, PB, EB... 등의 커다란 단위들이 등장하기 시작한다.
32bit 컴퓨터가 가진 CPU의 레지스터가 한 번에 처리할 수 있는 데이터의 용량은 32bit이기 때문에, 한 번에 표현 가능한 수의 최대값은 2의 32제곱이 된다. 2의 32제곱은 4,294,967,296이다. 즉, 레지스터가 한 번의 연산에서 표현할 수 있는 값의 크기는, CPU가 한 번에 인식하여 처리할 수 있는 주소값의 범위가 된다.
32비트는 이처럼 42억개정도의 메모리 공간을 사용할 수 있고, 메모리 공간 하나당 크기는 알아본 대로 1바이트다. 1바이트의 주소공간이 42억개 정도가 있으면, 메모리 용량 또한 42억어쩌구 바이트가 된다.
4,294,967,296 = 2^32 = 2^30 * 2^2 = 4GB (1GB = 2^30byte)
따라서, 32비트 컴퓨터에 4GB 이상의 메모리를 장착하더라도 인식되지 않는다.
위와 같은 방법으로 64비트 컴퓨터의 메모리 인식 범위도 계산할 수 있는데, 이는 16EB(엑사바이트)에 해당한다. 무어의 법칙에 따라, 어마어마하게 증가함을 알 수 있다.
어셈블리어 프로그램은 섹션으로 분리된다. data 섹션에는 할당할 데이터들이, text 섹션에는 코드가 위치한다.
어셈블리에서 사용하는 데이터의 단위는 다음과 같다.
데이터 타입 | 크기 |
---|---|
BYTE | 부호 없는 1바이트(8비트)정수 |
SBYTE | 부호 있는 1바이트(8비트)정수 |
WORD | 부호 없는 2바이트(16비트)정수 |
SWORD | 부호 있는 2바이트(16비트)정수 |
DWORD | 부호 없는 4바이트(32비트)정수 |
SDWORD | 부호 있는 4바이트(32비트)정수 |
FWORD | 48비트 정수 |
QWORD | 8바이트 (64비트) 정수 |
TBYTE | 10바이트 (80비트) 정수 |
위와 같은 데이터 타입들을 data 섹션이나 또 다른 섹션에서 사용하기도 한다.
64비트의 레지스터에는 rax, rbx, rcx, rdx, rdi, rsi 등이 존재한다.
어셈블리에서는 변수명 - 자료형 - 데이터 순으로 변수를 만들어 사용할 수 있고, 데이터 섹션에 들어가는 자료형에는 db, dw, dd등이 있다. 개행 문자는 0x0A로 표현한다.
어셈블리에서는 syscall을 사용하는 코드를 시작하기 전에 global _start라는 일종의 main함수를 선언해야 하고, 함수를 시작할 때에는 뒤에 :을 붙여서 구분 해 준다.
또한 모든 함수를 작성한 뒤에는 꼭 종료 함수를 사용하여 함수를 종료 해 주어야 한다.
; Save register
push REG
pop REG
; Set register value
mov REG, VALUE ; DEST = VALUE
; Common operations
add DEST, VALUE ; DEST = DEST + VALUE
sub - ; DEST = DEST - VALUE
inc REG ; REG++
dec - ; REG--
and DEST, REG ; DEST = DEST & REG
xor - ; DEST = DEST ^ REG
xor REG, REG ; = mov REG, 0
mul REG ; REG = REG * RAX
div REG ; REG = REG / RAX
; Dereferenced value
[REG] ; = *REG
; Compare
cmp REG, VALUE ; Set flags used by jmp variants
; Label
label:
jmp label ; next jumps depends on compare flags from cmp
je - ; is equal
jne - ; is not equal
jl - ; < VALUE
jle - ; <= VALUE
jz - ; = 0
jnz - ; != 0
jg - ; > VALUE
jge - ; >= VALUE
section .data ;데이터 영역임을 선언
msg db "hello world", 0x0A ;hello world 문자열과 개행 문자
section .text ;코드 영역임을 선언
global _start ;main
_start
mov eax, 4 ;eax레지스터에 write(출력)의 시스템 콜 번호 할당
mov ebx, 1 ;write(출력)의 표준 출력 (standart output)번호
mov ecx, msg ;write(출력)의 두번 째 인자에 입력한 메세지(msg)의 데이터 주소를 저장
mov edx, 12 ;write(출력)의 세번 째 인자에 문자열의 길이 저장
int 0x80 ;실행
mov eax, 1 ;eax레지스터에 exit(종료)의 시스템 콜 번호 할당
mov ebx, 0 ;정상적인 종료
int 0x80 ;실행
section .text
global _main
_main :
mov rax, 0x2000004
mov rdi, 1
mov rsi, msg
mov rdx, 12
syscall
mov rax, 0x2000001
mov rdi, 0
syscall
section .data
msg db "Hello World"
조건 점프 명령 | 산술, 논리 연산 | 의미 |
---|---|---|
JA | CMP a > b | Jump if above |
JB | CMP a < b | Jump if below |
JE | CMP a == b | Jump if equal |
JNE | CMP a != b | Jump if not equal |
JZ | TEST EAX, EAX (EAX=0) | Jump if zero |
JNZ | TEST EAX, EAX (EAX=1) | Jump if not zero |
CMP에 뒤따르는 첫번 째 operand에서 두번 째 operand을 뺐을 때, 그 결과가 마이너스인 경우에는 CF=1, 0일 경우에는 ZF=1 플래그가 설정된다. 플래그는 CPU의 플래그 레지스터(FLAG Register)에 저장되는 처리 데이터이다. FLAG의 한 비트가 한 플래그가 되고, 이 플래그의 설정 값에 따라 분기문의 조건이 달라진다.
이 분기문 조건의 상세한 내용은 여기를 참조했다.
여기도 참조해 보세요.
ZF | CF | |
---|---|---|
op1 > op2 | 0 | 0 |
op1 < op2 | 0 | 1 |
op1 == op2 | 1 | 0 |
더 자세한 설명은 여기에 잘 정리되어 있다.
이 정도면 얼추 된 것 같다.
우선 진행하면서 모르는 개념들은 더 추가하겠다.
strcpy는 string을 복사한다.
형식은 다음과 같다.
#include <string.h>
char * strcpy(char *dst, const char *src);
strcpy() 함수는 문자열의 끝을 알리는 \0
을 포함하여, src에 위치한 문자열을 dst로 복사한다. dst를 리턴한다.
ft_strcpy.c
char *ft_strcpy(char *dest, char *src)
{
int i;
i = 0;
while (src[i] != '\0')
{
dest[i] = src[i];
i++;
}
dest[i] = '\0';
return (dest);
}
목적지 dst는 rdi에, 소스 src는 rsi에 저장.
rax를 통해 리턴할 주소값 반환 (ft_strcpy의 char *형 리턴값)
ft_strcpy.s
section.text: ;코드가 위치할 영역임을 선언
global _ft_strcpy ;main
_ft_strcpy: ;실행할 코드 이름 및 내용
push rbx ;rbx 스택에 넣기
push rcx ;rcx 스택에 넣기
mov rax, rdi ;rdi(목적지)를 rax레지스터로 지정하기
mov rbx, rsi ;rsi(출발지)를 rbx레지스터로 지정하기
mov rcx, -1 ;rcx레지스터의 값을 -1로 지정하기
_while: ;
inc rcx ;rcx의 값 증가시키기
mov dl, byte [rbx + rcx] ;dl에 rbx + rcx번째 데이터를 넣기
mov byte [rax + rcx], dl ;rax + rcx번째 데이터에 dl값 넣기
cmp byte [rbx + rcx], 0 ;rbx + rcx번째 데이터를 0으로 만들기
jnz _while ;Jump if not zero -> _loop
pop rcx ;rcx 스택에서 꺼내기
pop rbx ;rbx 스택에서 꺼내기
ret ;return
strcmp 함수는 문자열을 비교하고 그 차이를 구한다.
형식은 다음과 같다.
#include <string.h>
int strcmp(const char *s1, const char *s2);
null문자로 끝나는 문자열 s1과 s2이 서로 같은지 비교한다.
Windows에서는,
아스키 코드 기준으로 문자열 s1이 s2보다 클 경우 1을 반환한다.
두 문자열이 같을 경우 0을 반환한다.
문자열 s1이 s2보다 작을 경우 1을 반환한다.
리눅스, OSX에서는
ASCII 코드값의 차이를 반환한다.
according to MAN
RETURN VALUES
The strcmp() and strncmp() functions return an integer greater than,
equal to, or less than 0, according as the string s1 is greater than,
equal to, or less than the string s2. The comparison is done using
unsigned characters, so that `\200' is greater than `\0'.
문자열 포인터에 NULL이 있을 시 에러가 발생한다.
중간에 다른 문자가 있을 시에는 s1, s2를 비교하여 s1-s2의 아스키 코드값을 리턴한다.
ft_strcmp.c
int ft_strcmp(char *s1, char *s2) {
unsigned int;
i = 0;
while (s1[i] != '\0' || s2[i] != '\0')
{
if (s1[i] > s2[i] || s1[i] < s2[i])
{
return (s1[i] - s2[i]);
}
i++;
}
return (0);
}
ft_strcmp.s
section.text: ;코드 영역 지정
global _ft_strcmp ;main
_ft_strcmp:
mov rcx, 0 ;rcx에 0대입
_loop: ;loop
mov al, byte [rdi + rcx] ;rdi는 s2, al에 rdi+rcx (목적지+0) 메모리 주소 대입
mov dl, byte [rsi + rcx] ;rsi는 s1, dl에 rsi+rcx (출발지+0) 메모리 주소 대입
cmp al, 0 ;al과 0을 비교. 비교값에 따라 플래그 지정.
jz _al_null ;0일 경우 _al_null로 점프
cmp dl ,0 ;dl과 0을 비교. 비교값에 따라 플래그 지정.
jz _dl_null ;0일 경우 _dl_null로 점프
cmp al, dl ;al, dl비교
ja _below ;above이면(al이 크면) _below로 이동
jb _above ;below이면(dl이 크면) _above로 이동
inc rcx ;rcx 1증가
jmp _loop ;_loop으로 이동
_al_null:
cmp dl, byte 0 ;dl, byte 0 비교
jz _equal ;0이면 _equal로 이동
jmp _below ;_below로 점프
_dl_null:
cmp al, byte 0 ;al, byte 0 비교
jz _equal ;0이면 _equal로 이동
jmp _above ;_above로 점프
_below:
mov rax, -1
ret
_above:
mov rax, 1
ret
_equal:
mov rax, 0
ret
write output.
형식은 다음과 같다.
#include <unistd.h>
ssize_t write(int fileds, const void *buf, size_t nbyte)
buf 포인터가 가리키는 버퍼에서 fileds 디스크립터에 의해 참조된 객체로 nbyte 만큼의 데이터를 쓴다. 성공할 시 쓰여진 바이트의 수가 리턴되고, 실패할 시 에러 표시를 위해 전역 변수 errono 가 세팅되며 -1이 리턴된다.
ft_write.s
global _ft_write
extern ___error
_ft_write:
push rbp
call ___error
pop rbp
mov rbx, rax
mov rax, 0x2000004
syscall
jc error
ret
error:
mov [rbx], rax
ret
ft_write의 내용 참조.
ft_read.s
section .text
global _ft_read
extern ___error
_ft_read:
mov rax, 0x2000003
syscall
jc _err
ret
_err:
push rax
call ___error
pop rdx
mov [rax], rdx
mov rax, -1
ret
문자열을 새로운 메모리에 복제하고, 복제된 메모리를 가리키는 포인터를 반환함.
프로토타입은 아래와 같음
#include <string.h>
char *strdup(const char *s1);
복제할 문자열 만큼의 메모리가 필요하므로 malloc 사용. 이 메모리 길이를 잴 때 밑에서 만들 ft_strlen과 ft_strcpy를 extern으로 불러오기.
ft_strdup
#include <stdlib.h>
int ft_strlen(char *str)
{
int i;
i = 0;
while (str[i] != '\0')
i++;
return (i);
}
char *ft_strcpy(char *dest, char *src)
{
int i;
i = 0;
while (src[i] != '\0')
{
dest[i] = src[i];
i++;
}
dest[i] = '\0';
return (dest);
}
char *ft_strdup(char *src)
{
int len;
int i;
char *parr;
if (src == NULL)
return (NULL);
len = ft_strlen(src);
i = 0;
parr = malloc(sizeof(char) * len);
while (src[i])
{
parr[i] = src[i];
i++;
len--;
}
parr[i] = '\0';
return (parr);
}
ft_strdup.s
section.text:
global _ft_strdup
extern _ft_strlen
extern _malloc
extern _ft_strcpy
_ft_strdup:
call _ft_strlen
inc rax
push rdi
mov rdi, rax
call _malloc
cmp rax, 0
jz _error
pop rsi
mov rdi, rax
call _ft_strcpy
ret
_error:
pop rdi;
ret
문자열의 길이를 재는 함수.
ft_strlen.c
int ft_strlen(char *str)
{
int i;
i = 0;
while (str[i] != '\0')
i++;
return (i);
}
ft_strlen.s
section.text:
global _ft_strlen
_ft_strlen:
mov rax, -1
_loop:
inc rax
cmp byte [rdi + rax], 0
jnz _loop
ret