X86_64 어셈블리 프로그래밍 기초

슈퍼컴퓨팅인프라센터 2022. 6. 2. 16:08

가. 어셈블리(Assembly) 언어란?

  • 어셈블리어란 사용자가 이해하기 어려운 기계어 대신에 명령 기능을 쉽게 연상할 수 있는 기호를 기계어와 대응시켜 코드화한 언어

  • 어셈블리어로 작성한 프로그램은 어셈블러를 통해 오브젝트 코드(기계어)로 변환하는 과정(어셈블)을 거쳐야 함

나. 어셈블리 언어를 배워야 하는 이유

  • 어셈블리 언어를 배워야 하는 이유는 다음과 같다: ※ 참고: x86-64 Assembly Language Programming with Ubuntu (Jorgensen)

  • 하드웨어 구조에 대한 이해를 돕는다. 기본적인 명령어, 레지스터, 메모리 접근방식, 하드웨어 인터페이싱에 대해 더 잘 이해할 수 있다.

  • 툴 체인에 대한 이해: 컴파일러, 어셈블러, 링커, 로더, 디버거와 같은 도구의 세부사항을 이해할 수 있다

  • 알고리즘 향상: 저수준 프로그램의 작성을 통해 프로그래머는 세부 사항에 더 많은 생각을 하게 된다.

  • 기능/절차에 대한 이해 : 함수/프로시저 호출이 작동하는 방식을 이해하게 되고, 스텍 기반의 인수, 보존 레지스터 및 동적 로컬 변수에 대한 개념을 이해할 수 있음

  • 컴파일러의 범위 이해: 컴파일러가 컴퓨터 아키텍처와 관련하여 하는 일과 하지 않는 일에 대해 이해할 수 있음

  • 공유메모리, 인터럽트, 스레드 처리, 경쟁 조건과 같은 개념에 대해 이해 할 수 있음

다. CISC vs RISC

1. CISC (Complex Instruction Set Computer)

  • 복잡한 명령어 집합을 갖는 CPU 아키텍처

  • 명령어가 복잡하기 때문에 명령어를 해석하는데 상대적으로 긴 시간이 필요하며 해석에 필요한 회로도 복잡함

  • 연산의 대상은 레지스터, 메모리, 또는 상수(immediate value)의 조합으로 다양함

  • 피연산자(operand)의 개수는 제한이 없으나, 2개~3개 지정하는 경우가 많음

  • 복잡한 명령어 처리를 위해 마이크로프로그램 방식을 채택하는 경우가 많음. 복잡한 명령을 다시 단순한 명령어(micro-instruction)로 나누어 명령어 파이프라인에서 처리함

2. RISC (Reduced Instruction Set Computer)

  • 명령어 개수를 줄여 하드웨어 구조를 단순하게 만드는 방식

  • CISC에서 지원하는 명령은 많지만, 그 중에서 실제로 자주 사용되는 명령어는 몇 개 되지 않는 다는 사실을 바탕으로 명령어 집합을 구성함

  • CISC에서 줄어든 제어 로직을 대신하여 레지스터와 캐시를 증가시켜 파이프라이닝(piplining) 기법 등을 적용하여 수행 속도가 전체적으로 향상됨

3. CISC 와 RISC의 비교

구분

CISC

RISC

하드웨어 복잡도

복잡한 하드웨어 구조

단순한 하드웨어 구조

명령어 개수

많은 명령어

최소 명령어

명령어 길이

다양한 길이

고정된 길이

실행 싸이클

복잡한 명령은 여러 클럭이 필요함

단순한 명령을 단일 클럭에 실행

메모리 참조

대부분의 명령이 메모리를 참조함

소수의 명령만 메모리를 참조함

명령어 실행

마이크로 프로그램이 명령을 실행

하드웨어가 직접 명령을 실행

어드레싱 모드

다양한 어드레싱 모드

단순한 어드레싱 모드

레지스터수

비교적 소수의 레지스터

비교적 레지스터 수가 많음

복잡도

마이크로 프로그램이 복잡

컴파일러 제작이 복잡함

파이프라이닝

파이프라인을 적용하기 힘듦

파이프라이닝이 쉬움

마이크로프로세서

X86

ARM, SPARC, MIPS

4. CISD 와 RISC 명령어 비교

예시 기능CISC 방식 어셈블리어RISC 방식 어셈블리어

덧셈 (메모리 어드레싱)

ADD Ma, Mb

LOAD Ra, Ma LOAD Rb, Mb ADD Ra, Rb STORE Ma, Ra

곱셈

mov ax, 10 mov bx, 5 mul bx, ax

mov ax, 0 mov bx, 10 mov cx, 5 begin: add ax, bx loop begin

라. 어셈블리 코딩 예제

다음과 같은 hello.asm 파일을 작성한다.

section .data
        text db "Hello, World!",10
section .text
        global _start
_start: mov rax, 1
        mov rdi, 1
        mov rsi, text
        mov rdx, 15    ; length of message
        syscall
        mov rax, 60
        mov rdi, 0
        syscall

빌드를 위한 nasm 어셈블러 설치한다.

(NASM: https://github.com/netwide-assembler/nasm , https://www.nasm.us/ )

$ sudo apt-get install nasm

컴파일하고, 링크하여 실행파일을 만들고 실행한다.

$ nasm -f elf64   -o hello.o  -l hello.lst  hello.asm 
$ ld hello.o  -o hello
$ ./hello 
Hello, World!
  • 옵션 -f 는 출력 파일의 형식으로 elf64는 리눅스를 위한 x86_64 포맷을 의미한다. nasm에서 지원하는 출력파일 형식은 nasm -hf 명령을 참고한다.

  • 옵션 -l 은 소스 코드와 매크로 처리 결과와 생성된 코드를 비교할 수 있도록 출력한다.

  • 코드 설명

    • data 섹션(.data)은 프로그램에서 초기화된 정적 변수를 위한 공간으로, 글로벌 변수와 정적 로컬 변수를 위한 공간이다. 이 섹션의 크기는 런타임에서 변경되지 않는다. data 섹션은 읽기와 쓰기가 가능하나, 읽기 전용을 위한 .rodata 섹션이 존재한다.

    • text 섹션(section .text)는 코드를 위한 영역으로 읽기만 가능하다.

    • _start 레이블은 프로그램의 엔트리 포인트이다. 이것은 디폴트 값으로 엔트리 포인트를 바꾸고 싶을 경우 ld -e foo 라고 할 수 있다.

    • x86_64 아키텍처의 레지스터는 다음과 같다. 자세한 것은 Intel 64 and IA-2 Architectures Software Developer's Manual 을 참고한다.

  • syscall 은 시스템콜을 호출하는 명령어로, 리눅스에서 사용하는 시스템 콜 번호는 관련 문서(64비트, 32비트, ARM)를 참고한다. 아키텍처에 따라서 시스템 호출 번호가 서로 다름을 알 수 있다.

  • 64비트 리눅스에서 sys_write 시스템콜 정의는 다음과 같다.

레지스터설명

%rax

1

sys_write

%rdi

1

unsigned int fd (표준출력:1)

%rsi

text

const char *buf

%rdx

15

size_t count

  • 시스템콜 sys_exit 의 정의는 다음과 같다.

레지스터설명

%rax

60

sys_exit

%rdi

0

int error_code

마. 더 자세한 내용이 궁금하시다면..

아래의 첨부파일을 참고하시기 바랍니다. 본 문서는 X86 64비트 아키텍쳐를 중심으로 일반적인 리눅스 환경에서 어셈블리 프로그래밍을 시작할 수 있는 기초 지식에 대해서 기술하였다. 어셈블리어 코딩, 역어셈블링 방법, C코드와 연동, SSE/AVX 확장 명령, ELF 포맷 등에 대한 기초 지식에 대해서 공부할 수 있다.

X86_64 어셈블리 프로그래밍 기초 2022.6.1. ISBN 978-89-294-1309-5-93560 한국과학기술정보연구원 슈퍼컴퓨팅기술개발센터 김상완, 오광진

본 보고서는 국가과학기술연구회에서 지원한 창의형 융합연구사업(CAP)인 "차세대 초고성능컴퓨터를 위한 이기종 매니코어 하드웨어 시스템 개발" 사업의 결과입니다. 무단전재 및 복사를 금지합니다.

Last updated