42 Seoul: libasm: ASM Syntax, Mandatory Part

Chaewon Kang·2021년 1월 12일
0

42 seoul

목록 보기
7/17

시작 전 개념 보충

우선 함수 작성 과정에서 기초 내용이 잘 정리된 블로그를 발견해서 링크합니다.

함수별 상세한 구현 과정에 대해서 여기도 참조하면 도움이 많이 됩니다.

아래는 공부하면서 찾아 본 내용.

어셈블리어(영어: Assembly language)는 기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어이다.컴퓨터 구조에 따라 사용하는 기계어가 달라지며, 따라서 기계어에 대응되어 만들어지는 어셈블리어도 각각 다르게 된다. 컴퓨터 CPU마다 지원하는 오퍼레이션의 타입과 개수는 제각각이며, 레지스터의 크기과 개수, 저장된 데이터 형의 표현도 각기 다르다. 모든 범용 컴퓨터는 기본적으로 동일한 기능을 수행하지만, 기능을 어떤 과정을 거쳐 수행할지는 다를 수 있으며, 이런 차이는 어셈블리어에 반영되게 된다.

어떤 프로그램이 만들어지면, 코드를 컴파일 하여 기계어로 번역한 다음, 오브젝트 파일을 만들고 해당 오브젝트 파일을 링커에 연결해 실행 가능한 파일로 만든다. 어셈블리어는 '컴파일' 다음의 결과물이라서, 이 파일을 가지고 오브젝트 파일을 만들어 링커로 실행 가능한 파일을 만들어 주어야 한다.

이 때 .s 파일을 오브젝트 파일 .o 파일로 만들어 주는 링커에 NASM, TASM, GCC등이 있고 주로 NASM을 사용한다.어셈블리 프로그래밍에서의 시스템 콜은 syscall 명령어를 사용하며, 32비트 운영체제와 62비트 운영체제가 다르다.

어셈블리어는 연산 명령(operation)과 연산 명령을 적용하는 대상(operand)로 구분된다.
예를들어 INC명령어의 경우, INC RSI, 1라는 식으로 사용할 수 있는데, 여기서 INC는 operation, RSI와 1은 operand이다.

operand에는 보통 레지스터가 온다. 메모리의 주소가 올 수도 있다. 숫자도 올 수 있다.

Register?

레지스터는 C언어에서의 변수처럼 사용된다.
범용 레지스터, 포인터 레지스터, 인덱스 레지스터, 세그먼트 레지스터... 등이 존재한다.

64비트 컴퓨터 CPU의 레지스터

RAX 레지스터
-> 누산기인 RAX 레지스터는 입출력과 대부분 산술 연산에 사용한다.
예를 들어 곱셈, 나눗셈, 변환 명령은 RAX를 사용한다.

RBX 레지스터
-> DS 세그먼트에 대한 포인터를 주로 저장. ESI나 EDI와 결합하여 인덱스에 사용된다.
메모리의 주소지정을 확장하기 위해 인덱스로 사용될 수 있는 유일한 범용 레지스터이다.

RCX 레지스터
-> 루프가 반복되는 횟수를 제어하는 값, 왼쪽이나 오른쪽으로 이동되는 비트 수 등을 포함.

RDX 레지스터
-> 입출력 연산에 사용하며 큰수의 곱셈과 나눗셈 연산에서 RAX와 함께 사용.

각각의 컴퓨터에는 Register 라는 CPU의 기억 장소가 존재한다. 즉, 레지스터는 cpu가 데이터를 담는 그릇이다. 어떤 프로그램에서 명령을 하면, CPU는 이 기억 장소(레지스터)에 값을 저장하고 연산을 진행한다. 컴퓨터의 버전에 따라 레지스터가 달라지는데, 32비트 컴퓨터가 가진 cpu의 레지스터가 한 번에 처리할 수 있는 처리값의 용량은 32bit이며, 마찬가지로 64비트 컴퓨터는 64비트이다.

레지스터에 대한 더 자세한 설명은 여기, 그리고 secho님의 블로그를 참조했다.

Bit / Byte?

C의 메모리 구조를 이해할 때나, CS관련 지식에서 가장 기초적인 부분을 다시 짚고 넘어가자. 컴퓨터는 결국 ON 아니면 OFF의 전기 신호를 가지고 모든 연산을 처리하게 되어 있어서, ON=1, OFF=0, 즉 1 아니면 0을 통해 모든 데이터를 표현하고, 받아들이고, 처리하고, 반환해야 한다.

비트는 1 아니면 0으로만 표현되는, 컴퓨터가 인식하는 최소 단위이다. bit는 단위가 너무 작아서 이를 일괄적으로 묶어 표현할 단위가, 여덞 개의 bit를 묶어 표현하는 byte라는 단위다. 컴퓨터의 디스크 혹은 메모리 공간은 비트단위로 쪼개져 있지만, 주소값의 단위는 byte로 표현된다.

컴퓨터 저장장치의 용량이 눈에 띄게 발전함에 따라, KB, MB, GB, TB, PB, EB... 등의 커다란 단위들이 등장하기 시작한다.

32bit 컴퓨터가 메모리를 4GB 사용하는 이유?

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(엑사바이트)에 해당한다. 무어의 법칙에 따라, 어마어마하게 증가함을 알 수 있다.

section data, text

어셈블리어 프로그램은 섹션으로 분리된다. data 섹션에는 할당할 데이터들이, text 섹션에는 코드가 위치한다.
어셈블리에서 사용하는 데이터의 단위는 다음과 같다.

데이터 타입크기
BYTE부호 없는 1바이트(8비트)정수
SBYTE부호 있는 1바이트(8비트)정수
WORD부호 없는 2바이트(16비트)정수
SWORD부호 있는 2바이트(16비트)정수
DWORD부호 없는 4바이트(32비트)정수
SDWORD부호 있는 4바이트(32비트)정수
FWORD48비트 정수
QWORD8바이트 (64비트) 정수
TBYTE10바이트 (80비트) 정수

위와 같은 데이터 타입들을 data 섹션이나 또 다른 섹션에서 사용하기도 한다.

64비트 컴퓨터 기준으로 어셈블러 바라보기

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

출처

콘솔에 Hello World를 프린트하는 어셈블리 프로그램 써보기

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"

조건부 점프

조건 점프 명령산술, 논리 연산의미
JACMP a > bJump if above
JBCMP a < bJump if below
JECMP a == bJump if equal
JNECMP a != bJump if not equal
JZTEST EAX, EAX (EAX=0)Jump if zero
JNZTEST EAX, EAX (EAX=1)Jump if not zero

CMP (Compare), TEST

CMP에 뒤따르는 첫번 째 operand에서 두번 째 operand을 뺐을 때, 그 결과가 마이너스인 경우에는 CF=1, 0일 경우에는 ZF=1 플래그가 설정된다. 플래그는 CPU의 플래그 레지스터(FLAG Register)에 저장되는 처리 데이터이다. FLAG의 한 비트가 한 플래그가 되고, 이 플래그의 설정 값에 따라 분기문의 조건이 달라진다.

이 분기문 조건의 상세한 내용은 여기를 참조했다.

여기도 참조해 보세요.

ZFCF
op1 > op200
op1 < op201
op1 == op210

기타...

더 자세한 설명은 여기에 잘 정리되어 있다.

이 정도면 얼추 된 것 같다.
우선 진행하면서 모르는 개념들은 더 추가하겠다.

Mandatory Part

ft_strcpy (man 3 strcpy)

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);
}

ASM 설계

목적지 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

ft_strcmp (man 3 strcmp)

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);
}

알아둬야 하는 내용

  • rbx 사용시 콜링 컨벤션에 따라 처음 값으로 되돌려 놓기

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 

ft_write (man 2 write)

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

알아둬야 하는 내용

  • size_t 는 unsigned int, ssize_t 는 signed int
  • 프로토타입의 fd, buf, size는 매개변수 순서와 똑같이 rdi, rsi, rdx를 통해 전달되어서 별도로 MOV 안 해줘도 됨
  • syscall을 사용하므로 errno가 출력되도록 작성하기. 여기참조.
  • ft_write와 ft_read는 syscall 호출 번호 부분 제외하고 똑같이 쓸 수 있음.

ft_read (man 2 read)

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

ft_strdup (man 3 strdup, malloc 허용)

문자열을 새로운 메모리에 복제하고, 복제된 메모리를 가리키는 포인터를 반환함.
프로토타입은 아래와 같음

#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 (man 3 strlen)

문자열의 길이를 재는 함수.

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
profile
문학적 상상력과 기술적 가능성

0개의 댓글