ABOUT ME

이 블로그는 다음 주소로 이전됩니다. https://faithfunchun.cafe24.com/ 이 블로그 이전은 티스토리의 사업 정책과 본 블로거의 입장이 상이한 부분이 있기에 진행하였습니다.

  • x86-64 Assembly Language 6 AT&T Syntax와 Intel Syntax
    Linux Development/Kernel 2021. 7. 31. 05:15

     

    thumbnail book shelves

    1. ISA (Instruction Set Architecture)

     ISA는 하드웨어 진영에서 소프트웨어 진영에 제공해주는 최종 결과물의 형태라고 보면 된다. 이 결과물 중엔 아래와 같은 것이 있다.

    • 어셈블리 명령(Instruction)들의 종류와 기계어로서의 각 명령(opcode)에 해당하는 이진수들에 대한 정보
    • 우리가 제공받는 레지스터의 종류와 역할, 기능에 관한 정보
    • 각 어셈블리 명령(opcode)들이 operands(인자)로 사용하는 레지스터들의 종류와 역할
    • 메모리 사용에 관한 정보(little endian 인지 또는 big endian 인지 등)
    • 세그먼트들의 논리적 구성 정보 (text, data, stack, heap)
    • 소프트웨어 context(흐름) 지원에 관한(RIP, EIP, context switching 등) 기능 정보
    • 명령 실행 후 그 결과를 나타내는 코드값들에 대한 정보(예외나 에러 처리)

     

    ISA는 일종의 규약으로 작용하기 때문에, 하나의 ISA 셋을 지원해주는 CPU들은(x86-64) 그것을 준수하는 많은 프로그램들을 자기 위에서 실행 가능하게 만들어준다.

    인텔의 x86-64 ISA를 IA-32라고 부른다. 이에 관한 CPU 스펙 정보는 인텔에서 오픈하고 있으며 아래 링크를 통해 pdf 파일로 다운로드할 수 있다.

     

     

    Intel® 64 and IA-32 Architectures Software Developer Manuals

    These manuals describe the architecture and programming environment of the Intel® 64 and IA-32 architectures.

    software.intel.com

     

    참고로 모든 ISA가 같은 방식으로 만들어지진 않는다. 같은 ISA를 지원해 주지만 그 밑의 하드웨어 구조는 다를 수 있다.

    하드웨어 진영에선 조금이라도 더 퍼포먼스를 끌어올리기 위해 무던한 노력을 하고 있기에 조금씩 바뀌는 그 변화들을 우리가 어느 정도 인지하고 있어야 하지만, 일단 ISA를 알고 있으면 된다.

     

    2. Low parts of Calling Conventions

    보통 콜링 컨벤션이라 함은 서브 루틴을 콜(함수, procedure call) 할 때, 스택 영역(지금까지의 설명에 있어왔던 애플리케이션 메모리 구조 속의 그 스택 영역)에 저장(push)되는 값들과 그것들을 넣는 순서, 콜 하는 과정에서 몇몇 레지스터에 무엇이 담겨야 하는지 등의 절차에 관한 규약이다.

    C나 C++ 코드를 실무에서 봐왔다면 아마 함수나 메서드를 선언하는 라인에 __stdcall, __fastcall, __cdecl 등의 단어를 본 적이 있을 것이다.

     

    어셈블러 프로그래밍을 위해서는 이런 콜링 컨벤션을 좀 더 기계와 밀접하게, 레지스터 하나하나가 어떻게 연관되어 있는지 구체적으로 알아야 할 필요가 있다. 그래서 이런 정보를 우리가 흔히 알고 있는 콜링 컨벤션의 내용과 구분하기 위해 제목에 'Low part of'를 붙였다.

     

    System V AMD64 ABI Calling Conventions

    • x86_64의 콜링 컨벤션, 일부는 하드웨어단 ISA에 정의돼 있고 일부는 OS가 정의한다.
    • 64bit 환경에서 최대 6개의 레지스터가 함수를 콜 할 때 인자를 전달하는 용도로 사용된다.
    • 6개의 레지스터: RDI, RSI, RDX, RCX, R8, R9 (64bit에 추가된 레지스터)
    • RAX는 리턴할 때 리턴 값을 담는 데 사용되며 부족할 경우(리턴 값이 64bit의 표현 범위를 넘는 경우) RDX까지 사용된다
    • 함수 인자가 6개가 넘을 경우엔 스택을 활용한다.
    • 64bit 환경에선 32bit 환경의 EDI, ESI와는 다르게 보통 RDI와 RSI 레지스터가 함수의 인자(Argument) 값을 담는 레지스터로 가장 자주 쓰인다.

     

    64bit registers with callee and caller marks

    Callee saved register

    • 서브루틴(callee, procedure)이 레지스터를 overwrite 하는 경우, 기존의 값을 보존해 뒀다가 차후 리턴할 때 복구해 줘야 할 의무가 있는 레지스터
    • RBX가 Callee saved register에 속한다.
    • 따라서 어셈블러 서브루틴(함수)의 첫 라인엔 이 RBX를 스택으로 미리 push 해두는 코드가 많이 보인다.
    • 서브루틴의 끝에는 스택에서 이 RBX값을 가져와(pop) 복원하고 ret 명령으로 상위 루틴으로 올라가는 라인이 많이 보인다.
    • 이런 Callee saved 레지스터의 성향 때문에 종종 함수의 도입부와 리턴부를 파악하기 쉬워진다.

     

    Caller saved register

    • 서브루틴(callee, procedure)이 해당 레지스 값의 보존을 책임지지 않으므로 필요한 경우 메인 루틴(caller)이 함수(callee)를 콜 하기 전에 알아서 보존할 필요가 있는 레지스터
    • RDX가 Caller saved register에 속한다.
    • 그래서 메인 루틴의(Caller) 코드 중 서브루틴(callee, procedure)을 콜 하는 라인 직전에 caller가 어떤 caller saved 레지스터의 값들을 스택이나 callee saved 레지스터에 보존해두는 코드가 종종 보인다.

     

    3. AT&T syntax

    • 명령어에 두 개 이상의 operand가 있을 때 데이터의 흐름은 좌측에서 우측이다.

     

    예를 들어 movq %rax, %rbx 라는 명령은 rax에 있는 데이터를 rbx로 넘기라는 뜻이다. (좌측이 Source 우측이 Destination)
    • 따로 지정하지 않을 경우 기본 엔트리 포인트는 _start 심벌(또는 레이블, 함수명, 변수명)로 정해진다.
    • 이 심벌은 global 지시자를 이용해(.global _start) 외부로 export한다.
    • 하나의 레이블(Label)을 선언하기 위해 콜론 ":" 문자를 접미사로 붙여준다.
    • 하나의 세그먼트에 들어갈 섹션을 만들기 위해 .섹션명: 처럼 표시해주며, 기본적으로 .text 섹션 .data 섹션 등이 있다.
    • AT&T에서는 이렇게 앞에 "."(period)가 들어간 단어들을 지시자(directive)라고 하며, cpu에게 전달할 인스트럭션이 아니라 어셈블리 컴파일러에게 전달할 내용임을 의미한다.
    • 지시자(directive)의 예로서 섹션 선언 이외에 자료형 선언(.string, .word 등)이 존재한다.
    • cpu는 인스트럭션을 해석하고 실행, 연산해줄 뿐, 어떤 종류의 데이터를 어디에 두고, 어떻게 다룰지에 대한 걱정은 작성된 코드와 컴파일러가 하는 것이다. 따라서 코드가 컴파일러에게 지시해주면 그것으로 충분하다.
    • 명령어가 다루는 데이터들의 크기를 표시하기 위해 명령(opcode) 뒤에 접미사를 붙이는데 아래와 같다.
    • 예를 들어 movq를 사용하면 기본적으로 64bit의 레지스터를 다루게 된다.
    • 참고로 q 어미를 가지고 있어도 기본 상수들은 최대 표현 범위가 4byte 일 수 있다.

     

    접미사 데이터 유형
    b byte 8bit
    s single 32 bit floating point
    w word 16 bit
    l long 32 bit integer or 64 bit floating point
    q quad 64 bit
    y ten bytes 10 bytes for 80 bit float point

    Operand Types

    • Immediate - 앞에 $를 붙여서 실제 데이터들을(immediate)을 직접 표현한다( $0x123, $-456 )
    • Register - 레지스터를 표시할 땐 앞에 %를 붙인다( %rax, %rbx, %r13 ... )
    • Memory - 괄호 "()"를 사용하여 메모리 어드레스를 포인팅 할 수 있다
    • 예를 들어 (%rax) 표시는 rax 레지스터 값을 주소로 써서 메모리에 접근한다
    • Operand가 두 개 이상일 때 양쪽 모두 Memory 타입을 쓸 수 있지만 양쪽 다 동시에 Memory 타입을 쓸 순 없다

     

    특정 세그먼트의 메모리 번지를 포인팅 할 때는 보통 그 세그먼트의 base 값과 offset 값이 더해져서 표현되는 경우가 많다. 이를 표현하기 위해 아래와 같은 형식이 사용된다.

     

    명령어 메모리 위치 (주소값)  기능
    movq 16(%rdx), %rax rdx +16  해당 위치의 값을
     rax로 넣어라
    movq (%rdx, %rcx) %rax rdx + rcx
    movq (%rdx, %rcx, 4) %rax rdx + ( 4 * rcx )
    movq 0x80( , %rdx, 2) %rax 0(없으니 0이다) + ( 2 * rdx ) + 0x80
    movq %ds:40(%rsi, %rdi, 2) %rax 데이터 세그먼트 시작번지 + rsi + ( 2 * rdi ) + 40
    • 이렇게 하나의 주소를 가리킬 때 %세그먼트 베이스:상수 오프셋(베이스, 레지스터 오프셋, 레지스터 오프셋의 배수) 형태로 표현될 수 있다.
    • 세그먼트 표시를 안 하면 어셈블러가 알아서 세그먼트를 선택하는데 이를 간접 주소 지정이라고 한다

     

    간접 지정되는 세그먼트 레지스터 오프셋 레지스터
    %cs %rip
    %ds나 %es중 적절한 곳 %rsi, %rdi, %rbx
    %ss %rsp, %rbp

     

    4. Intel syntax

    • 명령어에 두 개 이상의 operand가 있을 경우 데이터의 흐름은 우측에서 좌측이다.

     

    예를들어 mov eax, 0x4 라는 명령은 eax 레지스터에 0x4를 넣으라는 뜻이다. (좌측이 Destination 우측이 Source)
    • 기본 엔트리 포인트는 따로 정하지 않는 이상 global _start로 선언된다.
    • 하나의 세그먼트에 들어갈 섹션을 만들기 위해 section .섹션명: 처럼 사용한다. 기본적으로 .text, .data 섹션이 있다.
    • 명령어가 다루는 데이터의 크기를 정의하기 위해서 접미사를 쓰지 않는다.
    • 대신 해당하는 크기에 맞는 레지스터 명을 사용한다.
    • 메모리 접근하는 경우에는 데이터 크기를 따로 명시해준다.

     

    다루는 데이터 크기 레지스터 사용 예제 메모리 사용 예제
    8 bit mov al, cl mov al, byte ptr[bl]
    16 bit mov ax, cx add ax, word ptr[bx]
    32 bit cmp eax, ecx add eax, dword ptr[ebx]
    64 bit cmp rax, rcx mov rax, qword ptr[rbx]

    Operand Types

    • 데이터(Immediate) 10진수는 별다른 표시 없이 그냥 쓰면 된다, 16진수와 2진수는 접미사 b와 h를 가진다.(80h, 101b)
    • 레지스터는 그냥 레지스터명을 사용하면 된다. (mov rax, rcx)
    • Memory - 사각 괄호 "[]"를 사용하여 메모리 어드레스를 포인팅 할 수 있다. ( [eax] )

     

    특정 메모리 번지를 포인팅 하기 위한 베이스와 오프셋 표현은 아래와 같다.

     

    명령어 메모리 위치 (주소값)  기능
    mov rax, [rdx+30h] (or 30h[rdx]) rdx +0x30  해당 위치의 값을
     rax로 넣어라
    mov rax, [rdx+rcx] (or [rdx][rcx]) rdx + rcx
    mov rax, [rdx+rcx*4h] rdx + ( 4h * rcx )
    mov rax, [rdx+rcx*8h-40h] rdx + ( 8h * rcx ) - 0x40
    mov rax, ds:[rsi+2*rdi+80h] 데이터 세그먼트 시작번지 + rsi + ( 2 * rdi ) + 0x80
    • 주소 표현 방식은 Intel 문법이 더 직관적이다.
    • 주소 간접 지정 세그먼트는 AT&T 문법과 같다.

    댓글

© 2021. gadeokexp All rights reserved.