여러분이 리눅스 커널 개발자라면, 아키텍처에 의존적인 함수를 코딩하거나, 코드 경로를 최적화 하는 일을 많이 하게 된다. 어셈블리 언어 명령어를 C 문장(인라인 어셈블리로 알려진 메소드) 중간에 삽입하는 것이 대부분이다. 리눅스에서 인라인 어셈블리의 사용법에 대해 알아보자. (이 글에서는 IA32 어셈블리에 대해서만 다루겠다.)
먼저, 리눅스에 사용되는 기본적인 어셈블러 문법에 대해 알아보자, GCC(리눅스용 GNU C Compiler)는 AT&T 어셈블리 문법을 사용한다. 이 문법의 기본 규칙들 중 일부는 아래 소개된 것과 같다. (이 리스트가 결코 완벽한 것은 아니다. 인라인 어셈블리와 관련된 규칙들만 포함시켰다.)
레지스터 네이밍
레지스터 이름 앞에는 %가 붙는다. eax가 사용되어야 한다면, %eax로 해야 한다.
소스 및 목적지 순서
명령어에서, 소스가 먼저 오고 목적지가 뒤따른다. 이것은 Intel 문법과는 다르다. Intel 에서는 소스가 목적지 뒤에 온다.
mov %eax, %ebx, transfers the contents of eax to ebx.
|
피연산자의 크기
피연산자 byte, word, long에 따라서, 명령어 뒤에 b, w, l이 접미사로 붙는다. 의무적인 것은 아니다. GCC는 피연산자를 읽음으로써 올바른 접미사를 제공하려고 할 것이다. 접미사를 수동으로 설정하면 코드의 가독성을 높이고, 컴파일러가 잘못 추측할 가능성을 줄인다.
movb %al, %bl -- Byte move
movw %ax, %bx -- Word move
movl %eax, %ebx -- Longword move
|
Immediate operand
Immediate operand는 $를 사용하여 지정된다.
movl $0xffff, %eax -- will move the value of 0xffff into eax register.
|
간접 메모리 참조
메모리의 간접 참조는 ( )를 사용한다.
movb (%esi), %al -- will transfer the byte in the memory
pointed by esi into al register
|
GCC는 인라인 어셈블리를 위해 특별한 구조체 "asm"을 제공하는데, 다음과 같은 포맷이다.
asm ( assembler template : output operands (optional) : input operands (optional) : list of clobbered registers (optional) ); |
이 예제에서, 어셈블러 템플릿은 어셈블리 명령어로 구성된다. 인풋 피연산자는 명령어에 대한 인풋 피연산자로 작동하는 C 식이다. 아웃풋 피연산자는 어셈블리 명령어의 아웃풋이 수행될 C 식이다.
asm ("movl %%cr3, %0\n" :"=r"(cr3val));
|
a %eax b %ebx c %ecx d %edx S %esi D %edi |
메모리 피연산자 제약 조건(m)
피연산자가 메모리에 있을 때, 이에 대해 수행되는 연산은 메모리 위치에서 직접 발생한다. 이것은 먼저 값을 레지스터에 저장하여 수정되도록 한 다음, 이것을 다시 메모리 위치에 작성하는 레지스터 제약 조건에는 반대된다. 레지스터 제약 조건은 명령어에 반드시 필요할 경우 또는 프로세스의 속도를 많이 높일 경우에만 사용된다. 메모리 제약 조건은 C 변수가 "asm" 내에서 업데이트 되어야 하고, 레지스터에 값을 저장하고 싶지 않을 경우에 가장 효율적으로 사용된다. 예를 들어, idtr의 값은 메모리 위치 loc에 저장된다:
("sidt %0\n" : :"m"(loc));
|
매칭(Digit) 제약 조건
어떤 경우, 하나의 변수가 인풋과 아웃풋 피연산자로 작용한다. 이 같은 경우는 매칭 제약 조건을 사용하여 "asm"으로 지정된다.
asm ("incl %0" :"=a"(var):"0"(var));
|
우리 예제에서는, 레지스터 %eax가 인풋과 아웃풋 변수로서 사용된다. var 인풋은 %eax에 읽히고, 업데이트 된 %eax는 증분 후에 var에 저장된다. 여기에서 "0"은 0번째 아웃풋 변수와 같은 제약 조건을 지정한다. var의 아웃풋 인스턴스가 %eax에만 저장되어야 한다는 것을 지정한다. 이러한 제약 조건은 다음과 같은 경우에 사용될 수 있다.
- 인풋이 변수에서 읽히거나, 변수가 수정되고 수정이 다시 같은 변수에 작성될 경우
- 인풋과 아웃풋 피연산자의 개별 인스턴스들이 필요하지 않을 경우
매칭 제약 조건이 가장 큰 효과는 가용 레지스터들을 효율적으로 사용할 수 있다는 점이다.
다음 예제에서는 다양한 피연산자 제약 조건들의 사용법을 설명한다. 너무 많은 제약 조건들이 있어서 다 설명할 수 없지만, 지금 소개하는 것은 가장 자주 사용되는 제약 조건 유형들이다.
"asm"과 레지스터 제약 조건 "r"
먼저, 레지스터 제약 조건 'r'을 가진 "asm"을 보자. 우리 예제는 GCC가 레지스터를 할당하는 방법과 이것이 아웃풋 변수의 값을 업데이트 하는 방법을 설명한다.
int main(void)
{
int x = 10, y;
asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=r"(y) /* y is output operand */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}
|
이 예제에서, x의 값은 "asm" 안에 있는 y로 복사된다. x와 y는 레지스터에 저장됨으로써 "asm"으로 전달된다. 이 예제를 위해 생성된 어셈블리 코드는 다음과 같다:
main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%edx /* x=10 is stored in %edx */
#APP /* asm starts here */
movl %edx, %eax /* x is moved to %eax */
movl %eax, %edx /* y is allocated in edx and updated */
#NO_APP /* asm ends here */
movl %edx,-8(%ebp) /* value of y in stack is updated with
the value in %edx */
|
GCC는 "r" 제약 조건이 사용될 때 레지스터를 자유롭게 할당한다. 우리 예제에서는 x를 저장할 때 %edx를 선택했다. %edx에서 x의 값을 읽은 후에, y에도 같은 레지스터를 할당했다.
y가 아웃풋 피연산자 섹션에 지정되기 때문에, %edx에 업데이트 된 값은 스택 상의 y의 위치인 -8(%ebp)에 저장된다. y가 인풋 섹션에 지정되었다면, y(%edx)의 임시 레지스터 스토리지에서 업데이트 되더라도, 스택 상의 y의 값은 업데이트 되지 않는다.
%eax가clobbered 리스트에 지정되기 때문에, GCC는 데이터를 저장할 때 다른 곳에서 이것을 사용하지 않는다.
인풋 x와 아웃풋 y가 같은 %edx 레지스터에서 할당되었고, 아웃풋이 생성되기 전에 인풋이 소비된 것으로 간주한다. 여러분이 많은 명령어를 갖고 있다면, 상황은 달라진다. 인풋과 아웃풋이 다른 레지스터에 할당되었는지를 확인하려면, & 제약 조건 수정을 지정한다. 다음은 제약 조건 수정이 추가된 예제이다.
int main(void)
{
int x = 10, y;
asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=&r"(y) /* y is output operand, note the
& constraint modifier. */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}
|
다음은 이 예제를 위해 생성된 어셈블리 코드이다. x와 y가 "asm"을 통해 다른 레지스터에 저장되었음이 분명하다.
main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%ecx /* x, the input is in %ecx */
#APP
movl %ecx, %eax
movl %eax, %edx /* y, the output is in %edx */
#NO_APP
movl %edx,-8(%ebp)
|
이제, 피연산자용 제약 조건으로서 개별 레지스터를 지정하는 방법에 대해 알아보자. 다음 예제에서, cpuid 명령어는 %eax 레지스터에서 인풋을 가져다가, 네 개의 레지스터 %eax, %ebx, %ecx, %edx에 아웃풋을 준다. cpuid에 대한 인풋(변수 "op")는 eax 레지스터에 있는 "asm"으로 전달된다. a, b, c, d 제약 조건은 아웃풋에 사용되어 네 개의 레지스터에서 각각 값들을 모은다.
asm ("cpuid"
: "=a" (_eax),
"=b" (_ebx),
"=c" (_ecx),
"=d" (_edx)
: "a" (op));
|
이를 위해 생성된 어셈블리 코드는 다음과 같다. (_eax, _ebx 등의 변수들이 스택에 저장된 것으로 간주함.):
movl -20(%ebp),%eax /* store 'op' in %eax -- input */
#APP
cpuid
#NO_APP
movl %eax,-4(%ebp) /* store %eax in _eax -- output */
movl %ebx,-8(%ebp) /* store other registers in
movl %ecx,-12(%ebp) respective output variables */
movl %edx,-16(%ebp)
|
strcpy 함수는 다음과 같은 방식으로 "S"와 "D" 제약 조건을 사용하여 구현될 수 있다:
asm ("cld\n
rep\n
movsb"
: /* no input */
:"S"(src), "D"(dst), "c"(count));
|
소스 포인터 src는 "S" 제약 조건을 사용하여 %esi에 놓이고, 목적지 포인터인 dst는 "D" 제약 조건을 사용하여 %edi에 놓인다. 카운트 값은 %ecx에 놓인다. rep 접두사에 필요하기 때문이다.
다음은 두 개의 레지스터 %eax와 %edx를 사용하여 두 개의 32-bit 값을 결합하고 64-bit 값을 생성하는 제약 조건이다:
#define rdtscll(val) \
__asm__ __volatile__ ("rdtsc" : "=A" (val))
The generated assembly looks like this (if val has a 64 bit memory space).
#APP
rdtsc
#NO_APP
movl %eax,-8(%ebp) /* As a result of A constraint
movl %edx,-4(%ebp) %eax and %edx serve as outputs */
Note here that the values in %edx:%eax serve as 64 bit output.
|
다음은 네 개의 매개변수들을 가진 시스템 호출용 코드이다:
#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}
|
위 예제에서, 시스템 호출에 대한 네 개의 인자들은 제약 조건 b, c, d, S를 사용함으로써, %ebx, %ecx, %edx, %esi에 놓인다. "=a" 제약 조건은 아웃풋에 사용되어, %eax에 있는 시스템 호출의 리턴 값이 __res 변수에 놓이게 한다. 매칭 제약 조건 "0"을 인풋 섹션의 첫 번째 피연산자 제약 조건으로서 사용함으로써, syscall 넘버 __NR_##name은 %eax에 놓이고, 시스템 호출에 대한 인풋으로서 작동한다. 따라서, %eax는 여기에서 인풋과 아웃풋 레지스터로서 작동한다. 어떤 개별 레지스터들도 이러한 목적으로 사용되지 않는다. 또한 인풋(syscall 넘버)는 아웃풋(syscall의 리턴 값)이 만들어 지기 전에 소비(사용)된다.
다음과 같은 원자 감소 연산을 생각해 보자:
__asm__ __volatile__(
"lock; decl %0"
:"=m" (counter)
:"m" (counter));
|
이것을 위해 생성된 어셈블리는 다음과 같다:
#APP
lock
decl -24(%ebp) /* counter is modified on its memory location */
#NO_APP.
|
이 카운터를 위해 레지스터 제약 조건을 사용하는 것을 생각할 수도 있다. 만약 그렇다면, 카운터의 값은 먼저 레지스터에 복사되고, 감소된 다음, 메모리로 업데이트 되어야 한다. 하지만, 잠금과 원자성이라는 순수한 목표를 잃었다. 이것은 메모리 제약 조건을 사용해야 할 필요성을 증명하는 것이다.
메모리 카피의 기본적인 구현에 대해 생각해 보자.
asm ("movl $count, %%ecx;
up: lodsl;
stosl;
loop up;"
: /* no output */
:"S"(src), "D"(dst) /* input */
:"%ecx", "%eax" ); /* clobbered list */
|
lodsl이 %eax를 수정하는 동안, lodsl과 stosl 명령어는 이를 모호하게 사용한다. %ecx 레지스터는 카운트를 명확하게 로딩한다. 하지만, GCC는 우리가 알려주지 않는 한 이것을 모른다. 이것은 clobbered 레지스터 세트에 %eax와 %ecx를 포함시킴으로써 우리가 하는 일이다. 이것이 수행되지 않는 한, GCC는 %eax와 %ecx가 비어있다고 간주하고, 다른 데이터를 저장하는데 사용하기로 결정한다. %esi와 %edi 는 "asm"에 의해 사용되고 clobbered 리스트에 없다. 이것은 "asm"이 인풋 피연산자 리스트에서 이들을 사용하기로 선언했기 때문이다. 밑에 있는 라인은 레지스터가 "asm" 안에서(명확하게 또는 모호하게) 사용되고, 이것이 인풋 또는 아웃풋 피연산자 리스트에 없다면, 여러분이 이것을 clobbered 레지스터로서 리스팅 해야 한다.
인라인 어셈블리는 이 글에서 다 다루지 못할 정도로 거대하고 많은 기능을 갖고 있다. 이 글에서 기초적인 것을 공부했으니, 여러분이 직접 인라인 어셈블리를 코딩 해 보는 것도 좋을 것 같다.
-
Using and Porting the GNU Compiler Collection (GCC) 매뉴얼.
-
GNU Assembler (GAS) 매뉴얼.
-
Brennan의 Inline Assembly 가이드.
Bharata B. Rao는 인도 Mysore University에서 전자 통신 엔지니어링을 전공했다. 1999년부터 IBM Global Services에서 일하고 있다. IBM Linux Technology Center의 멤버로서, 리눅스 RAS (Reliability, Availability, Serviceability) 분야를 담당하고 있다. 운영 체계 내부 구조와 프로세스 아키텍처에 관심이 많다. (rbharata@in.ibm.com)