[libasm] 어셈블리 프로그램 구조와 레지스터 이해하기

hidaehyunlee·2020년 11월 2일
0

42SEOUL

목록 보기
14/14

1. 어셈블리어 ?

"기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어" - 위키백과

어셈블리어는, 0과 1로만 이루어져 있는 기계어에 MOV, ADD와 같은 명령어를 각각 대응시킨, 프로그래밍의 저급 언어이다. 컴퓨터 구조(CPU)마다 기계어가 다르기 때문에 이에 대응하는 어셈블리어도 각각 달라지게 된다.

이러한 단점을 개선하기 위해 만들어진 것이 C언어와ㄴ 같은 고급 언어로, CPU에 종속적이지 않으면서도, 저급 언어처럼 메모리에 직접 접근할 수 있다는 장점이 있다.

  고급 언어(.c) --컴파일러--> 어셈블리어(.s) --어셈블러--> 기계어(.o) --링커--> 실행 파일

고급 언어의 경우에는 프로그램을 실행하기까지 위와 같은 과정을 거쳐야 한다. 하지만 어셈블리어 파일은 이미 .s확장자를 가지고 있기 때문에, 바로 어셈블러를 거쳐 오브젝트 파일을 생성할 수 있다.

파일의 크기가 작고 동작 속도가 빠른 특징 덕분에 요즘은 초소형 임베디드 시스템에서 많이 사용된다.


2. 프로그램 구조

2.1. section

어셈블리 프로그램은 아래 세 개의 section으로 구성되어 있다.

  • section.data
    • 초기값이 있는 전역 변수, 혹은 스태틱 변수를 선언하는 공간
    • 상수, 파일 이름, 버퍼 사이즈 등을 여기에 선언할 수 있다.
  • section.text
    • 실행할 코드를 작성하는 공간
  • section.bbs
    • 추가적으로 변수를 선언할 때 사용하는 공간

2.2. 문법

어셈블리어는 정해진 표준이 없고, CPU에 따라 여러 종류의 문법이 존재한다. libasm과제에서는 x64 Intel 문법을 사용하며 포맷은 아래와 같다.

Opcode Operand1, Operand2 ;주석
  • opcode는 명령어, operand는 인자값이다.
    • operand2가 source인자이고, operand1이 Destination인자이다.
    • 참고로 AT&T 문법에서는 src와 dest가 반대. <- 헷갈리니까 잊어버리기.
  • 숫자는 1,2,3, ... 그대로 표기한다.
  • 특정 레지스터의 메모리 주소를 참조할 때는 대괄호([])를 사용한다.
  • Offset: EAX 레지스터에서 +4 만큼 떨어진 메모리 주소를 표기할 때는 [EAX + 4] 와 같이 표현한다.
  • 세미콜론으로 주석을 처리한다.

3. x64 범용 레지스터

레지스터는 CPU가 요청을 처리하는 데 필요한 데이터(명령어의 종류, 연산결과, 복귀주소 등)를 일시적으로 저장하는 기억장치이다. CPU에서 사용하는 변수라고 생각하면 레지스터를 조금 쉽게 이해할 수 있다.

Operand로 다룰 수 있는 몇 가지 레지스터를 알아보자.

  1. RAX (Accumulator) : 더하기, 빼기 등 산술/논리 연산을 수행하며 함수의 return값이 저장된다.
    • 시스템콜 함수를 사용하려면 RAX에 함수의 syscall 번호를 넣어준다.
  2. RBX (Base) : 메모리 주소를 저장하기 위한 용도로 사용된다.
  3. RCX (Count) : 반복문에서 카운터로 사용되는 레지스터. 고급언어 for문의 i 와 같은 역할이지만, 다만 ECX는 미리 반복 값을 정해두고 명령어를 사용할 때마다 값이 하나씩 줄어든다는 점이 다르다.
    • syscall을 호출했던 사용자프로그램의 return 주소를 가진다.
  4. **RDX **(Data) : 다른 레지스터를 서포트하는 여분의 레지스터. 큰 수의 곱셈이나 나눗셈 연산에서 EAX와 함께 사용된다.
  5. RSI (Source Index) : 데이터를 복사할 때 src데이터, 즉 복사할 데이터의 주소가 저장된다.
  6. **RDI **(Destination Index) : 데이터를 복사할 때 복사된 dest데이터의 주소가 저장된다.
  7. RSP (Stack Point) : 스택프레임에서 스택의 끝 지점 주소가 저장된다.
    • 즉, 데이터가 계속 쌓일 때 스택의 가장 높은 곳을 가리킨다.
    • 주소상으로는 스택은 높은 주소에서 낮은 주소로 쌓이니 스택의 가장 낮은 주소를 가리키고 있다.
    • push, pop 명령을 통해 ESP 값이 위아래로 8바이트씩 이동하면서 스택프레임의 크기를 변경하게 된다.
  8. RBP (Base Point) : 함수가 호출되면 스택프레임이 형성 되는데 이 스택스레임의 시작 지점 주소가 저장된다.
  9. R8 ~ R15
    • 32비트에서는 위 8개의 범용 레지스터를 사용한다. 물론 이름이 다르고(R 대신 extended의 E. EAX, ESP 등등..) 레지스터 크기가 4바이트였다.
    • 64비트에서는 R8 ~ R15까지 8개의 레지스터가 추가로 사용된다.

4. 명령어(opcode) 종류

Opcode에서 사용하는 어셈블리 명령어는 엄청나게 많다. 자주 사용한다는 몇 가지 명령어만 정리해봤다. 코드를 보면서 추가 예정.

4.1. 조작 명령어

  • call : 함수 호출
  • ret : call로 호출된 함수를 종료하고 그 다음 명령줄로 이동
  • jmp : 분기(라벨) 실행
  • nop : 아무것도 하지 않음

4.2. 데이터 전송 명령어

  • push : 스택에 값을 넣음
  • pop : 스택에서 값을 가져옴
  • mov : 인자2 값을 인자1에 대입(전달)
  • lea : 인자2 주소를 인자1에 대입(전달)

4.3. 산술 명령어

  • inc : 인자의 값을 1 증가
  • dec : 인자의 값을 1 감소
  • add : 인자2 값을 인자1에 더함
  • sub : 인자2 값을 인자1에서 뺌
  • cmp : 인자1,2의 값을 비교

5. 시스템 콜

_main:
    mov rax, 0x2000004 ;시스템콜 함수를 write로 변경
    syscall ;시스템콜(write) 호출
  • syscall 명령어를 통해 시스템 상에 미리 선언되어 있는 함수를 사용할 수 있다.

  • 이 때 syscall 할 함수의 번호를 rax에 미리 넣어줘야한다.

    • 반환값 또한 rax에 저장된다.
  • Mac의 syscall 테이블 표

    • 0x2000001 - exit()
    • 0x2000002 - read()
    • 0x2000003 - write()
    • 0x2000004 - write()
    • 0x2000005 - open()
    • 0x2000006 - close()
  • syscall 후 에러 발생 시 이는 __error 함수를 이용해 처리해야한다.

  • __error 함수는 sys/errno.h 에 선언되어 있다.


6. 예제

sancho님의 블로그에서 참고한 예제. 이해하는데 너무 도움 되었습니다. 감사합니다!

; asm.s

section .data

    message db "Hello, world!", 10 ; 아스키코드로 10은 LF(라인피드)

section .text
global _main

_main:
    mov rax, 0x2000004 ; 시스템콜 함수를 write로 변경
		;아래 코드는 write(1, message, 14)와 같다고 볼 수 있다.
    mov rdi, 1 ; fd를 표준 출력인 1로 변경
    mov rsi, message ; rsi는 출력할 주소에 해당
    mov rdx, 14 ; 출력할 크기를 14로 변경
    syscall ; 시스템콜(write) 호출
		mov rax, 0x2000001 ; 시스템콜 함수를 exit로 변경
		syscall ; 시스템콜(exit) 호출

컴파일

nasm -f macho64 asm.s
  • 만약 nasm 설치가 안되어 있다면 brew install nasm

링킹

ld -lSystem asm.o 

실행

./a.out

7. 참고

profile
삽질의 기록들 👨‍💻

0개의 댓글