Libasm

DaewoongJeon·2021년 2월 25일
1

42seoul Subject

목록 보기
3/8

1. 과제 해석

A. 과제 소개

이번 과제의 목표는 어셈블리어에 대해 이해하고, 몇가지 간단한 라이브러리들을 어셈블리어로 코딩하는 것이다.

  1. Introduction
    An assembly (or assembler) language, often abbreviated asm, is a low-level programming language for a computer, or other programmable device, in which there is a very strong (but often not one-to-one) correspondence between the language and the architecture’s machine code instructions. Each assembly language is specific to a particular computer architecture. In contrast, most high-level programming languages are generally portable across multiple architectures but require interpreting or compiling. Assembly language may also be called symbolic machine code.
    어셈블리(assembly) 언어는 컴퓨터 또는 기타 프로그래밍 가능한 장치를 위한 낮은 수준의 프로그래밍 언어로서, 언어와 아키텍처의 기계 코드 명령 사이에 매우 강력한 (종종 일대일이 아닌) 대응 관계가 있다. 각 어셈블리 언어는 특정 컴퓨터 아키텍처에 따라 다릅니다. 대조적으로, 대부분의 고급 프로그래밍 언어는 일반적으로 여러 아키텍처에서 이동 가능하지만 해석 또는 컴파일이 필요하다. 어셈블리어는 심볼 머신 코드라고도 합니다.

  2. Common Instructions

  • You must write 64 bits ASM. Beware of the "calling convention".
    64비트 ASM을 작성해야 합니다. "calling convention"에 주의하십시오.
  • You can’t do inline ASM, you must do ’.s’ files.
    inline ASM을 수행할 수 없으며 '.s' 파일을 수행해야 합니다.
  • You must compile your assembly code with nasm.
    어셈블리 코드를 nasm으로 컴파일해야 합니다.
  • You must use the Intel syntax, not the AT&T.
    AT&T가 아닌 Intel 규격을 사용해야 합니다.
  1. Mandatory part
  • The library must be called libasm.a.
    과제의 library은 libasm.a이다.
  • You must submit a main that will test your functions and that will compile with your library to show that it’s functional.
    함수를 테스트하고 라이브러리와 컴파일할 수 있는 main을 제출하여 기능을 나타내야 합니다.
  • You must check for errors during syscalls and properly set them when needed
    시스템 호출 중에 오류가 있는지 확인하고 필요할 때 적절하게 설정해야 합니다.
  • Your code must set the variable errno properly.
    코드는 변수 오류를 맞게 설정해야 합니다.
  • For that, you are allowed to call the "extern ___error".
    이를 위해 "extern __error"을 호출할 수 있습니다.

2. 과제 수행 과정

A. 어셈블리어란?

기계어는 실제로 컴퓨터의 CPU가 읽어서 실행할 수 있는 0과 1로 이루어진 명령어의 조합이다. 이러한 각 명령어에 대해 사람이 알아보기 쉬운 니모닉 기호(mnemonic symbol)를 정해 사람이 좀 더 쉽게 컴퓨터의 행동을 제어할 수 있도록 한 것이 어셈블리 언어이다. 기계어와 1대1 대응이 되는 프로그래밍 저급 언어이다.

B. 작업 환경 세팅

  1. brew 설치(일반적인 방법으론 클러스터에 설치가 안됨. nano 권한 때문인듯.) : rm -rf $HOME/.brew && git clone --depth=1 https://github.com/Homebrew/brew $HOME/.brew && export PATH=$HOME/.brew/bin:$PATH && brew update && echo "export PATH=$HOME/.brew/bin:$PATH" >> ~/.zshrc
  • 소소한 문제 : brew 설치 중 write error: No space left on device8.01 MiB | 13.94 MiB/s 이런 문구가 뜨면서 설치가 중단이 됐다. 직감적으로 공간이 부족한거같다.. 42 slack tool bot을 통해 i mac 캐시들을 삭제하는 방법을 알아냈다. clean : rm -r ~/Library/Caches/*; rm ~/.zcompdump*; brew cleanup
  1. 어셈블러 nasm 설치 : brew install nasm

C. Hello World

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"
  1. section .data
    : 초기값이 있는 전역 변수나 static 변수를 선언하는 공간
  2. section .text
    : 작성된 코드가 저장되는 공간
  3. section .bbs
    : 추가적인 변수 선언이 필요할 때 사용하는 공간
  • global _main
    : 어셈블리 함수에 _를 붙이는 이유는 일종의 약속때문이라고 한다, 어셈블리는 기본적으로 모든 코드가 private하기 때문에 global instruction을 이용하여 심볼에 다른 코드가 접근할 수 있도록 해 준다.

  • msg db "Hello World"
    : msg는 변수명이고 db는 데이터 타입이다. 그리고 msg안에 "Hello World"문자열이 입력된다.
    (db : byte (1byte), dw : word (4byte), dd : double (8byte))

  • 문법
    Opcode Operand1, Operand2 ;주석
    : 기본적으로 어셈블리 코드 문법은 위와 같은 format을 따른다.

  1. opcode에는 어셈블리 명렁어가 들어간다.
  2. operand에는 인자값이 들어온다. operand1은 목적지, operand2는 출발지이다.(인자값으로 레지스터가 들어오는 경우도 있다.)
  3. 세미콜론으로 주석을 처리한다.
  • assmble
  1. helloworld.s -> helloworld.o : nasm -f macho64 hello.s
    (어셈블리어 파일을 오브젝트파일로 변환시켜준다. 옵션 -f의 인자로 macho64를 안주게 되면 어셈블러가 64비트os 기준으로 동작을 안한다.)
  2. ld -lSystem hello.o -o hello : 오브젝트 파일들을 링크해준다. 어셈블리 파일에서 system call을 사용했을 경우, 옵션으로 -lSystem을 주어 system call 라이브러리를 사용한다고 알려야 한다.
  • 아래에서 레지스터 어셈블리 명령어 system call에 대해 구체적으로 알아보자

D. 레지스터

CPU가 요청을 처리하는 데 필요한 데이터(명령어의 종류, 연산결과, 복귀주소 등)를 일시적으로 저장하는 기억장치이다. CPU 내부에 위치하고 다른 메모리공간보다 데이터에 훨신 빠르게 접근할 수 있지만 비싸다는 단점이 있다.

CPU 내부엔 다양한 레지스터가 있지만 어셈블리어와 직접적으로 관련된 프로그램 실행 관련 레지스터만 다뤄볼 것이다.

1) 범용 레지스터

상수나 주소를 저장하거나 특수한 목적으로 사용되는 레지스터이다.(가장 많이 쓰임)

A) 산술 연산 레지스터

  1. AX : 산술 논리 연산에 사용되고 함수의 리턴값이 저장된다.
  2. BX : 메모리 주소를 저장한다.
  3. CX : 반복문의 반복횟수 지정에 사용된다.(counter) 문자열 처리에도 사용된다.
  4. DX : AX의 보조 역할이다.

B) 인덱스 레지스터

  1. SI(Source Index) : 복사나 비교할 때 사용되는 소스 문자 주소의 index를 가리킨다.
  2. DI(Destination Index) : 복사나 비교할 때 사용되는 목적지 문자 주소의 index를 가리킨다.

C) 포인터 레지스터

  1. SP(Stack Pointer) : 스택의 가장 윗부분을 가리킨다. 스택에 값이 쌓이면 SP도 역시 증가한다.
  2. BP(Base Pointer) : 스택의 바닥 부분의 주소를 가리킨다. BP밑은 return 값이 있다.
  • 레지스터 이름의 접두사로 R이나 E가 오는 경우가 있는데, 운영체제에 따라 달라진다.(ex. x64 OS : RAX, x32 OS : EAX)

2) Flag 레지스터

시스템 제어용 플래그로 사용되거나, 어셈블리의 조건 처리, 상태 저장 용도로 사용된다. Flag 레지스터의 참, 거짓에 따라 분기하게 된다.

A) 연산 결과

  1. CF (Carry Flag) : 덧셈과 뺄셈에서 빌림수 발생시 1로 설정
  2. PF (Parity Flag) : 연산 결과가 짝수이면 1, 홀수면 0으로 설정
  3. AF (Auxiliary carry Flag) : 16(8)비트 연산 할 때, 빌림수 발생시 1로 설정
  4. ZF (Zero Flag) : 연산 결과가 0이면 1로 설정
  5. SF (Sign Flag) : 연산 결과 최상위 비트가 1인 경우 1로 설정
  6. OF (Overflow Flag) : 연산 결과가 용량을 초과하였을 경우 1로 설정

B) 시스템 제어

  1. TF (Trap Flag) : 프로그램 추적시 1로 설정, 명령을 한 행씩 실행한다.
  2. IF (Interupt enable Flag) : 외부 인터럽트 요구를 받아들일 때 1로 설정
  3. AC (Alignment check) : CR0 레지스터의 AM 비트와 함께 1로 설정하면 메모리 참조 시 정렬 체크를 활성화 한다.
  4. IOPL (I/O Privilege level) : 현재 특권 수준이 IOPL보다 높은 경우에 I/O 주소 접근, 11이 가장 낮음
  5. NT (Nested task) : 연결 작업 제어, 1로 설정 시 현재 작업이 기존 실행 작업과 연결됨을 의미함.
  6. RF (Resume Flag) : 디버깅 시 프로세서에 일시 중지 예외 발생 제어
  7. VM (Vitual-8086 mode) : Virtual-8086-mode 활성화 시 1로 설정
  8. VF (Virtual Interrupt Flag) : 가상 이미지의 인터럽트 요구를 받아 들일 때 1로 설정 됨. VIP와 같이 사용된다.
  9. VIP (Virtual Interrupt Pending) : 가상 모드 인터럽트가 지연시 1로 설정.
  10. ID (ID Flag) : CPUID 명령어 지원 여부로 1로 설정 시 지원

C) 문자열 제어

  1. DF (Direction Flag) : 문자열 복사 명령과 관련된 제어 플래그, 1로 설정되어 있으면 문자열 복사 시 주소값이 감소.

E. 어셈블리 명령어

1) 조작 명령어

  • call : 함수 호출
  • ret : call로 호출된 함수를 종료하고 그 다음 명령줄로 이동
  • nop : 아무것도 하지 않음
  • jmp : 분기(라벨) 실행.
  • 조건 점프 명령어 : cmp 연산 결과에 따라 jmp
  1. je : cmp A B 에서 A = B 일 때 특정 라벨로 jmp
  2. jne : cmp A B 에서 A != B 일 때 특정 라벨로 jmp
  3. ja : cmp A B 에서 A > B 일 때 특정 라벨로 jmp
  4. jb : cmp A B 에서 A < B 일 때 특정 라벨로 jmp
  5. jae : A >= B
  6. jbe : A <= B

2) 데이터 전송 명령어

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

3) 산술 명령어

  • inc : 인자의 값을 1 증가
  • dec : 인자의 값을 1 감소
  • add : 인자2 값을 인자1에 더함
  • sub : 인자2 값을 인자1에서 뺌
  • cmp : 인자1, 2의 값을 비교. 주로 위의 조건점프 명령어와 세트로 사용한다.
  • test : 인자1과 인자2를 And 연산한다. 이 연산의 결과는 ZF에만 영향을 미치고 operand 자체에는 영향을 미치지 않는다.
  1. 보통 rax의 값이 0인지 확인할 때 rax 0. 0 이런 식으로 사용된다.
  2. 만약 TEST의 연산결과가 0이라면 ZF는 1로, 연산결과가 1이라면 ZF는 0으로 세트된다.

F. System call

  • system call table : https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
  • 어셈블리를 활용하면 system call 함수에 직접 접근하여 사용할 수 있다.(기존 방식은 커널을 통해 활용해야 하는 방식이다.)
  • rax에 주소값을 입력할 때, 앞에 2를 붙이는 이유는 1로 시작하는 주소값은 BSD layer이기 때문이다.(추후 자세히 알아볼 예정)
  • system call 함수 접근 방식
  1. rax 레지스터에 사용하고자 하는 함수에 해당하는 주소값을 입력한다.
    : write 함수 활용 : mov rax, 0x2000004
  2. 매개변수 순서대로 해당하는 레지스터에 값을 입력한다.
  3. syscall 명령어를 작성한다.

G. BYTE, WORD, DWORD

  1. BYTE : 특정 주소의 공간을 1 BYTE 크기로 선언한다.
  2. WORD : 특정 주소의 공간을 2 BYTE 크기로 선언한다.
  3. DWORD : 특정 주소의 공간을 4 BYTE 크기로 선언한다.
  • 접두사로 S가 붙으면 부호를 고려한다.
  • cmp BYTE [rdx + rcx], 0 : rdx + rcx 주소부터 1 BYTE 크기 만큼 1 BYTE 크기의 0과 비교해준다.
  • mov WORD [rdx + rcx], 5 : rdx + rcx 주소에 1 BYTE 크기의 숫자 5을 넣어줍니다.
  • 주의사항 : operand로 들어오는 두 개의 인자는 서로 크기를 맞춰주어야 한다.

H. 함수의 매개변수

  • 호출된 함수의 매개변수는 어셈블리로 밑의 순서로 입력된다. (7번째 매개변수부터 스택으로 들어온다.)
    rdi -> rsi -> rdx -> %r10 -> %r8 - > %r9

I. 음수 출력하기

이 부분은 계속 알아보는 중이다.

  • neg : 정의 자체는 부호를 변환시켜주는 어셈블리 명령어이다. 하지만 unsigned 형의 값에 적용할 경우, 부호 비트를 고려하지 않아서 부호가 바뀌는 대신 2의 보수 + 1 값으로 바뀐다.
  • ft_strcmp 함수를 어셈블리로 구현할 때, 입력된 두 개의 문자열 s1, s2 중에서 두 번째로 입력된 s2가 클 경우, 음수 값을 출력해야 한다. 하지만 오류가 발생하였다.
  1. al 레지스터에 return 값을 넣어서 반환하려고 했지만 음수 값을 입력 하고 반환하면 양수인 2의 보수 + 1 값이 반환 되는 오류를 발견하였다. 이를 통해 해당 레지스터는 부호 비트를 고려하지 않는 것 같다라는 결론을 도출하였다.
  2. rax 레지스터는 부호비트를 고려하는 것 같다. mov 어셈블리 명령어를 활용하 rax 레지스터에 -1을 입력하여 반환하였더니 그대로 출력하였다. 추후에 rax 레지스터를 활용하는 방안을 고려해볼 것이다.

J. 전역, 정적 변수 선언

어셈블리에서도 역시 전역 및 정적 변수를 선언하여 사용할 수 있다.

  • db : 1byte, dw : 2byte, dd : 4byte
  • 선언 방식은 다음과 같다.
  1. var db 64
    : 1byte 변수 var에 0x64값을 넣어 선언한다.
  2. var db ?
    : 1byte 변수 var를 아무것도 넣지 않고 선언한다.
  3. msg db "Hello World"
    : 1byte 변수 msg에 "Hello World"문자열을 넣어 선언한다.

K. 에러 처리

system call 함수나 외부함수를 활용할 떄 그에 따른 에러처리를 해주어야 한다. 에러처리를 위해 ___error 외부함수를 사용하였다.

  1. system call로 불려진 함수에 오류가 발생할 경우, carry flag가 1이 되고, errno를 rax에 반환한다.
  2. jc 어셈블리 명령어는 carry flag가 1일 경우, jmp시켜주는 명령어로 에러가 발생 했을 때, err구문으로 jmp할 수 있도록 코딩해준다.
  3. err구문엔 errno를 반환해주는 코딩을 할 것이다.
  4. push를 활용하여 반환된 errno를 스택에 백업시켜둔다.
  5. call ___error외부함수를 실행시켜서 rax로 에러가 발생한 부분의 주소를 반환받는다.
  6. 반환 받은 주소에 errno를 받기 위해서 스택에 백업된 errno를 pop해준 후에 rdx레지스터에 담는다. 그리고 mov [rax], rdx 코딩을 하여 rax 주소 공간에 errno를 넣는다.
  7. 에러 발생 시 write함수는 음수 값을 반환해야 하므로 마지막에 rax레지스터에 -1을 넣어준다.

L. 메모리 활용 원리

원래는 데이터의 백업이 필요할 때 레지스터에 백업하는 방식을 활용하였으나, 오류가 발생하여 스택백업방식으로 전부 바꿨다. 레지스터 백업방식에 왜 오류가 발생하는지 알기 위해서 어셈블리가 메모리를 어떻게 활용하는지에 대한 이해가 수반되어야할 것 같았다.

M. Calling Convention

3. 참고

0개의 댓글