학습자료(~2017)/어셈블리어

AT&T Assembly Code (어셈블리어)

단세포소년 2011. 5. 2. 06:34
반응형

Linux Assembly Code


글쓴이 : 이호 (i@flyduck.com)
최신 글이 있는 곳 : http://linux.flyduck.com/

v0.1.0 2000년 3월 28일


차례
0. 서문
1. GAS와 AT&T 문법
2. Inline Assembly
3. Reference



0. 서문
이 문서는 리눅스에서 사용하는 어셈블리 문법에 대해서 (특히 x86에서) 간략히 요약한 글입니다. GAS와 AT&T 문법에서는 어셈블리 코드의 형식과 이것이 인텔에서 사용하는 문법과 어떤 차이가 있는지를 나타냅니다. 이 부분은 인텔에서 사용하는 어셈블리(Macro Assembler나 Turbo Assembler)를 알고 있다면 많은 도움이 될 것입니다. Inline Assembly는 C 코드내에서 어셈블리 코드를 사용하는 방법에 대한 글입니다. 커널 코드에서 CPU에 의존적인 부분들의 상당수는 inline 어셈블리 코드로 작성되어 있는데, 이 형식에 낯선 사람들이 이를 이해하는데 도움이 되리라 생각합니다. 이 글은 x86 어셈블리 코드에 대해 기본적인 지식이 있다고 가정하고 있습니다.
이 문서는 Linux Assembly HOWTO 문서와 Brennen's Guide to Inline Assembly, DJGPP QuickASM Programming Guide, GCC Manual, GAS Manual에 있는 내용을 요약 정리한 것입니다. 그대로 사용한 코드들도 많아서 어떤 면에서는 정리했다기 보다는 그냥 옮겼다고 해도 무방할 듯 합니다 (^^;). 여기서 다루는 내용외에 더 자세한 것을 바란다면 마지막 장 Reference에 나오는 문서들을 보시기 바랍니다. 특히 x86 Assembly Language FAQ 문서는 어셈블리에 대해서 낯선 분에게도 큰 도움이 될 것입니다. 실제 CPU 명령어에 대해서는 각 CPU 제조회사에서 제공하는 매뉴얼을 참조하시기 바랍니다. 그럼 리눅스를 공부하시는 분께 도움이 되길 바랍니다.




1. GAS와 AT&T 문법
GAS는 GNU Assembler로서 GCC와 함께 쌍으로 사용되는 Assembler이다. 이는 32-bit UNIX Compiler를 위해 만들어졌으므로, UNIX에서 일반적으로 사용되는 AT&T 문법을 따른다. 이 문법은 Intel에서 사용하는 문법과는 많이 다르다. 이를 비교해보면 :

Register 이름 : 모든 레지스터 이름에는 %가 앞에 붙는다. 이는 레지스터와 다른 심볼들을 혼동하지 않게 하는 장점이 있다. 예를 들어 eax 레지스터를 나타낼 때 :
AT&T : %eax
Intel : eax


참고로 Intel 80386에는 다음과 같은 레지스터가 있다 :



일반 register : %eax (%ax, %ah, %al), %ebx (%bx, %bh, %bl), %ecx (%cx, %ch, %cl), %edx (%dx, %dh, %dl), %esi (%si), %edi (%di), %ebp (%bp), %esp (%sp) : 32-bit, 16-bit, 8-bit 레지스터로 사용가능
section register : %cs, %ds, %es, %fs, %gs, %ss
processor control register : %cr0, %cr1, %cr3
debug register : %db0, %db1, %db2, %db3, %db6, %db7
test register : %tr6, %tr7
floating point register stack : %st => %st(0), %st(1), %st(2), %st(3), %st(4), %st(5), %st(6), %st(7)

Operand 순서 : 원본(source)이 왼쪽, 목적지(destination)가 오른쪽에 위치한다. Intel 문법에서는 이와 반대로 되어 있다. 예를 들어 eax에서 ebx 레지스터로 데이터를 복사하는 것은 :
AT&T : movl %eax, %ebx
Intel : mov ebx, eax



Operand 크기지정 : operand의 크기를 지정할 때 크기에 따라 b (byte), w (word), l (long) 접미사를 명령어에 붙인다. operand로 지정된 레지스터를 가지고 크기를 판단할 수 있을 때는 이를 생략할 수 있으며, 판단할 수 없는 경우 32-bit 연산으로 가정하게 된다. operand가 레지스터가 아니라 메모리인 경우는 반드시 접미사를 붙이는게 좋다. Intel에서는 byte ptr, word ptr, dword ptr 같은 지시자(specifier)를 사용하여 이를 나타낸다. 예를 들어 word크기의 foo 값을 bx로 복사하는 것은 :
AT&T : movw foo, %ax
Intel : mov ax, word ptr foo


AT&T와 Intel에서 사용하는 명령어들은 대부분 비슷하지만 몇가지 다른게 있다. sign extend 명령어와 zero extend 명령어는 약간의 차이를 보이는데, AT&T 문법에서는 이러한 extend 명령어에 원래의 크기와 확장할 크기를 지정하게 한다 :

movsSD (sign extend, Intel에서는 movsx)
movzSD (zero extend, Intel에서는 movzx)


여기서 S는 원래의 크기 D는 목적하는 크기이다. 예를 들어 ax를 ecx로 sign extend를 한다면 :

AT&T : movswl %ax, %ecx
Intel : movsx ecx, ax


또한 몇가지 변환함수에도 차이가 있다 :

cbtw (sign extend byte (%al) to word (%ax), Intel에서는 cbw)
cwtl (sign extend word (%ax) to long (%eax), Intel에서는 cwde)
cwtd (sign extend word (%ax) to long (%dx:%ax), Intel에서는 cwd)
cltd (sign extend dword (%eax) to quad (%edx:%eax), Intel에서는 cdq)



상수(constant)와 immediate 값 : 모든 상수와 immediate 값에는 $가 붙는다. 예를 들어 숫자 5를 나타내려면 $5라고 한다. 변수의 주소를 나타낼 때에도 앞에 $를 붙인다. 예를 들어 foo란 변수의 주소는 $foo이다. 그냥 foo라고 하면 변수의 값을 나타내게 된다. foo 변수의 주소를 eax 레지스터로 복사하는 것은 :
AT&T : movl $foo, %eax
Intel : mov eax, foo


foo의 값을 eax 레지스터로 복사하는 것은 :

AT&T : movl foo, %eax
Intel : mov eax, [foo]


숫자 0xd00d를 eax 레지스터로 복사하는 것은 :

AT&T : movl $0xd00d, %eax
Intel : mov eax, 0d000h



메모리 참조 : AT&T와 Intel에서 메모리 주소를 참조하는 방법(indexing, indirection)에는 표기법상 약간의 차이가 있다.
AT&T : section:immed32(base, index, scale)
Intel : sectioin:[base + index * scale + immed32]


이는 base + index * scale + immed32 주소를 나타내게 된다. 이들 모두를 반드시 지정해야 하는 것은 아니지만 immed32나 base 중의 하나는 반드시 지정해야 한다. 주소 지정의 예를 들어보자. eax 레지스터가 가리키는 주소의 값을 참조하는 경우 :

AT&T : (%eax)
Intel : [eax]


레지스터에 변수(var)의 옵셋을 합한 주소를 참조하는 경우 :

AT&T : var(%eax)
Intel : [eax + var]


4바이트 단위로 된 정수 배열(int array[])의 eax번째 값을 참조하는 경우 (int array[eax]) :

AT&T : array(, %eax, 4)
Intel : [eax * 4 + array]


위 배열에서 ebx 인덱스에서 시작하여 eax번째 값을 참조하는 경우 (int array[ebx + eax]) :

AT&T : array(%ebx, %eax, 4)
Intel : [ebx + eax * 4 + array]



jump/call/return : long jump나 long call에서는 다음과 같은 차이가 있다 :
AT&T : ljmp/lcall $section, $offset
Intel : jmp/call far section:offset


far retrun의 경우 :

AT&T : lret $stack-adjust
Intel : ret far stack-adjust



그밖에 : AT&T Assembler에서는 여러개의 section을 지원하지 않는다. UNIX 프로그램에서는 모든 프로그램이 하나의 section에 있다고 생각을 한다.



2. Inline Assembly
inline assembly는 high-level 언어로 된 코드 중간에 넣어서 사용하는 어셈블리 코드로, 네가지 항목으로 구성되며, 다음과 같은 형식으로 사용한다.
__asm__(어셈블리 문장 : 출력 : 입력 : 변경된 레지스터);

각 항목은 콜론(':')으로 구분되며, 어셈블리 문장은 반드시 들어가야 하지만, 뒤의 세 항목은 필요에 따라서 넣거나 생략할 수 있다. 각 항목은 다음과 같은 의미를 가진다.


어셈블리 문장 (assembly statement) : AT&T 형식으로 만들어진 어셈블리 코드로 "" 안에 넣어서 작성하며 각각의 줄은 newline으로 구분된다.
출력 (output) : 어셈블리 코드에서 출력용으로 사용하는 레지스터/메모리를 변수와 연결시켜준다. 여러개를 지정할 수 있으며 각 항목은 쉼표(',')로 구분된다. 각 항목은 "=g"(var)같은 형식을 가진다.
입력 (input) : 어셈블리 코드에서 입력으로 사용하는 레지스터/메모리를 변수와 연결시켜준다. 여러개를 지정할 수 있으며 각 항목은 쉼표(',')로 구분된다. 각 항목은 "g"(var)같은 형식을 가진다.
변경된 레지스터 (registers-modified 또는clobbered regiser) : 어셈블리 코드에서 컴파일러가 모르는 사이에 바뀔 수 있는 레지스터의 목록을 기술한다. 각 항목은 "" 안에 들어가며, 여러개의 항목을 넣을 때에는 쉼표(',')로 구분한다. 메모리에 있는 변수의 값을 수정하는 경우 "memory"라고 기술해주어야 한다.
예제 코드를 보면 :

__asm__ ("pushl %eaxn"
"movl $1, %eaxn "
"popl %eax"
);

이 코드는 eax 레지스터를 저장하고 여기에 1을 입력했다가 eax 레지스터를 원래의 값으로 복구하는 코드이다. 여기서는 아무런 입력이나 출력이 없으며, 변경되는 레지스터도 없으므로 어셈블리 코드만 존재한다. 이제 i라는 변수를 하나 증가시키는 코드를 만들어보자.


int i = 0;

__asm__ ("pushl %%eaxn"
"movl %0, %%eaxn"
"addl $1, %%eaxn"
"movl %%eax, %0n"
"popl %%eax"
: /* no output variable */
: "g" (i)
);

우선 이 코드에서 모든 레지스터 앞에 %가 두개가 붙어있는데, 입력이나 출력, 변경된 레지스터 중의 하나라도 기술을 하는 경우, 레지스터 이름에는 %를 하나가 아니라 두개를 붙여야 한다. 이는 내부에서 %0, %1 하는 식의 기호가 사용되는데 이것과 혼동되는 것을 막기 위해서이다. 이 코드에서는 출력이 없으므로 출력은 비워 두었다. 입력에는 "g"(i)라고 적혀 있는데, 이는 i라는 변수를 %0과 연결시켜주는 역할을 한다. 즉 코드내에서 %0은 변수 i와 같은 의미로 사용된다. 따옴표 안에 있는 것은 변수와 어떤것이 연결되는지를 말하는데 g는 이경우 컴파일러가 알아서 레지스터에 넣던지 메모리에 두던지 하라고 지시하는 것이다. 따옴표 안에는 다음과 같은 것을 지정할 수 있다.


a eax
b ebx
c ecx
d edx
S esi
D edi
I 상수 (0에서 31) ("I"라고 사용하는게 아니라 "0" 처럼 숫자를 넣어서 사용)
q eax, ebx, ecx, edx 중 동적으로 할당된 레지스터
r eax, ebx, ecx, edx, esi, edi 중 동적으로 할당된 레지스터
g eax, ebx, ecx, edx 또는 메모리에 있는 변수. 컴파일러가 선택
A eax 와 edx를 결합한 64-bit 정수

%0은 입력에서 지정한 변수를 가리킨다. 즉 여기서는 i라는 변수를 가리키게 된다. 입력에서 여러개를 기술하면, 기술한 순서대로 차례로 %0, %1, ... 의 이름을 갖게 된다.


int x = 1, x_times_5;

__asm__ ("leal (%1, %1, 4), %0"
: "=r" (x_times_5)
: "r" (x)
);

위 코드는 x라는 변수를 다섯배 곱하여 x_times_5에 저장한다. ((%1, %1, 4) = %1 + %1 * 4 = %1 * 5, lea는 주소를 저장하라는 명령이므로 %0에 %1을 다섯배한 값이 들어가게 된다). 여기서는 결과를 저장해야 하므로 출력에 "=r"(x_times_5)라고 출력되는 변수를 지정하였다. 따옴표안에 =가 들어가는 것은 출력임을 나타내기 위해서이다. 이 코드를 조금 수정하여 x를 다섯배 곱하여 x에 이 값을 넣는다면 :


__asm__ ("leal (%1,%1,4), %0"
: "=r" (x)
: "0" (x)
);

여기서 입력에 "0"이라고 숫자로 썼는데, 이는 앞에서 지시한 것을 다시 가리키는 경우이다. 순서에 따라 출력 "=r"은 %0, 입력 "0"은 %1이 되는데, 이 둘을 같은 것을 가리키게 하고 싶은 경우 "0"이라고 하여 %0과 같은 것이라고 지시해주는 것이다. 즉 여기서 %1은 %0과 같은 것이 된다. 그 래서 이 코드는 x를 다섯배를 곱하여 결과를 자기 자신에서 돌려주게 된다. 입출력을 같이 하는 예로 k = i + j를 예로 들면 :


int i = 1, j = 2, k;

__asm__ __volatile__ ("pushl %%eaxn"
"movl %1, %%eaxn"
"addl %2, %%eaxn"
"movl %%eax, %0n"
"popl %%eax"
: "=g" (k)
: "g" (i), "g" (j)
);

순서에 따라 k = %0, i = %1, j = %2가 되고, %1 + %2를 %0에 저장하여 k = i + j 값이 들어가게 된다. 여기서 __asm__ 다음에 __volatile__가 있는데, 이는 이 코드를 지정한 위치에 그대로 두라는 것이다. 컴파일러는 최적화(optimization)를 하는 과정에서 코드의 위치를 옮길 수 있는데 이를 막는 것이다.


#define rep_movsl(src, dest, numwords)
__asm__ __volatile__ (
"cldn"
"repn"
"movsl"
:
: "S" (src), "D" (dest), "c" (numwords)
: "%ecx", "%esi", "%edi"
);

위 코드는 src에서 dest로 지정한 길이만큼 복사하는 것이다. 이 코드를 실행하면 edx, esi, edi 레지스터가 변경되게 되므로, 마지막에 변경된 레지스터 목록에 이 세개를 지정해주었다.




3. Reference
Linux Assembly HOWTO :

영문판 : http://linuxdoc.org/HOWTO/Assembly-HOWTO.html
한글판 : http://kldp.org/HOWTO/Assembly-HOWTO
Brennen's Guide to Inline Assembly :

http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html
DJGPP QuickASM Programming Guide :

http://www.castle.net/~avly/djasm.html
x86 Assembly Language FAQ :

http://www.faqs.org/faqs/assembly-language/x86/
GCC Manual (GCC Inline Assembly) :

http://gcc.gnu.org/onlinedocs/gcc_toc.html
GAS Manual :

http://www.gnu.org/manual/gas-2.9.1/as.html

http://www.ibm.com/developerworks/kr/library/l-gas-nasm.html
리눅스 어셈블러 : GAS 와  NASM 비교(한글)

반응형