-
x86-64 Assembly Language 6 AT&T Syntax와 Intel SyntaxLinux Development/Kernel 2021. 7. 31. 05:15
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) 값을 담는 레지스터로 가장 자주 쓰인다.
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 문법과 같다.