C.2. 사례 분석

리눅스 커널에 이미 사용된 수 많은 예를 통해 어떤 식으로 인라인 어셈블리가 사용됐는지 알아보자.

C.2.1. strcpy()

아래 소스 코드는 include/asm-i386/string.h에 있는 strcpy() 함수를 가져와 컴파일 해보기 위해 조금 추가한 코드다.

	/* test.c */
	static inline char * strcpy(char * dest,const char *src)
	{
	int d0, d1, d2;
	__asm__ __volatile__(
		"1:\tlodsb\n\t"
		"stosb\n\t"
		"testb %%al,%%al\n\t"
		"jne 1b"
		: "=&S" (d0), "=&D" (d1), "=&a" (d2)
		:"0" (src),"1" (dest) : "memory");
	return dest;
	}

	int main()
	{
		char a[] = "1234";
		char b[] = "4567";

		strcpy(a, b);

		return 0;
	}
	

컴파일은 'gcc -S -c test.c'라고 한다. 그러면 test.s가 생길 것이다. test.s는 다음과 같다.

		.file	"test.c"
		.version	"01.01"
	gcc2_compiled.:
	.section	.rodata
	.LC0:
		.string	"1234"
	.LC1:
		.string	"5678"
	.text
		.align 4
	.globl main
		.type	 main,@function
	main:
		pushl %ebp
		movl %esp,%ebp
		subl $24,%esp
		leal -8(%ebp),%eax
		movl .LC0,%edx
		movl %edx,-8(%ebp)
		movb .LC0+4,%al
		movb %al,-4(%ebp)
		leal -16(%ebp),%eax
		movl .LC1,%edx
		movl %edx,-16(%ebp)
		movb .LC1+4,%al
		movb %al,-12(%ebp)
		addl $-8,%esp
		leal -16(%ebp),%eax
		pushl %eax
		leal -8(%ebp),%eax
		pushl %eax
		call strcpy
		addl $16,%esp
		xorl %eax,%eax
		jmp .L3
		.p2align 4,,7
	.L3:
		movl %ebp,%esp
		popl %ebp
		ret
	.Lfe1:
		.size	 main,.Lfe1-main
		.align 4
		.type	 strcpy,@function
	strcpy:
		pushl %ebp
		movl %esp,%ebp
		subl $28,%esp
		pushl %edi
		pushl %esi
		pushl %ebx
		movl 12(%ebp),%esi
		movl 8(%ebp),%edi
#APP
		1:	lodsb
		stosb
		testb %al,%al
		jne 1b
#NO_APP
		movl %esi,%ecx
		movl %edi,%edx
		movl %ecx,%ebx
		movl %ebx,-4(%ebp)
		movl %edx,%edx
		movl %edx,-8(%ebp)
		movl %eax,%eax
		movl %eax,-12(%ebp)
		movl 8(%ebp),%eax
		jmp .L2
	.L2:
		leal -40(%ebp),%esp
		popl %ebx
		popl %esi
		popl %edi
		movl %ebp,%esp
		popl %ebp
		ret
	.Lfe2:
		.size	 strcpy,.Lfe2-strcpy
		.ident	"GCC: (GNU) 2.95.3 20010315 (release)"
	

인라인 어셈블리는 #APP와 #NO_APP사이에 존재한다.

: "=&S" (d0), "=&D" (d1), "=&a" (d2)

output의 구성을 나타낸다. "=&S" (d0)는 d0를 'si' 레지스터에 저장하는 것이고 "=&D" (d1)은 d1을 'di' 레지스터에 저장하란 것이고 "=&a" (d2)는 d2를 'a' 레지스터에 저장하란 것이다.

test.s에 의하면 어셈블리 코드가 실행된 후 output으로 d0, d1, d2가 있는데 #NO_APP 바로 밑의 3줄이 이 역할을 한다. d2는 %ebx에 할당됐음을 알 수 있다.

:"0" (src),"1" (dest)

input의 구성을 나타낸다. "0" (src)는 src가 0번째 오퍼랜드와 같은 위치를 점유하란 말로 %0인 d0를 의미한다. 또 d0가 si를 사용하므로 결국 si의 초기 값이 src가된다. dest는 %1인 di에 입력된다.

test.s에 의하면 #APP 바로 전의 두줄이 input에 해당하고 %esi와 %edi에 src, dest를 입력해 준다.

: "memory"

clobber에 지정된 "memory"는 컴파일러에게 어셈블리코드가 메모리의 어딘가를 변경한다고 가르쳐 주는 것이다. 이 것을 사용하지 않으면 어셈블리코드에서 메모리의 내용을 변경하는 것을 컴파일러는 전혀 알 수 없다. 잘 못하면 어셈블리에서 고친 값과 다른 값을 컴파일러 는 사용하고 있을 가능성도 있다. "memory"를 명시해 주면 컴파일러는 어셈블리 코드를 실 행하기 전/후에 레지스터에 저장되어 있는 모든 변수의 값을 갱신하도록 한다.

"1:\tlodsb\n\t"

1:은 label을 의미한다. loadsb 명령으로 al 레지스터에 es:esi의 내용을 읽어 온다. 여기서 src의 내용을 읽어 온다. 명령 실행후 esi는 자동으로 1이 증가한다(바이트 단위로 읽기 때문).

"stosb\n\t"

al의 값을 es:edi에 저장한다. edi도 명령 실행 후 1 증가한다.

"testb %%al,%%al\n\t"

al의 내용이 0인지 테스트한다. 스트링을 복사할 땐 NULL 캐릭터가 나올 때 까지 복사하기 때문에 0인지 판별한다.

"jne 1b"

0이 아닌 경우, 즉 NULL 캐릭터가 아닌 경우 계속해서 복사한다.

C.2.2. _set_gate()

arch/i386/kernel/trap.c에 있는 _set_gate()의 내용을 가져다 컴파일 하기 위해 약간 변경한 것이다.

/* sg.c */

#define __KERNEL_CS 0x10

#define _set_gate(gate_addr,type,dpl,addr) \
do { \
  int __d0, __d1; \
  __asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
	"movw %4,%%dx\n\t" \
	"movl %%eax,%0\n\t" \
	"movl %%edx,%1" \
	:"=m" (*((long *) (gate_addr))), \
	 "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
	:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	 "3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \
} while (0)

int main()
{
	_set_gate(0, 1, 2, 3);
	return 0;
}

'gcc -S -c sg.c'로 컴파일한 것은 다음과 같다.

	.file	"sg.c"
	.version	"01.01"
gcc2_compiled.:
.text
	.align 4
.globl main
	.type	 main,@function
main:
	pushl %ebp
	movl %esp,%ebp
	subl $24,%esp
	nop
	.p2align 4,,7
.L3:
	movl $3,%edx
	movl $1048576,%ecx
	movl %ecx,%eax
#APP
	movw %dx,%ax
	movw $-16128,%dx
	movl %eax,0
	movl %edx,4
#NO_APP
	movl %eax,%ecx
	movl %ecx,-4(%ebp)
	movl %edx,%eax
	movl %eax,-8(%ebp)
.L5:
	jmp .L4
	.p2align 4,,7
.L6:
	jmp .L3
	.p2align 4,,7
.L4:
	xorl %eax,%eax
	jmp .L2
	.p2align 4,,7
.L2:
	movl %ebp,%esp
	popl %ebp
	ret
.Lfe1:
	.size	 main,.Lfe1-main
	.ident	"GCC: (GNU) 2.95.3 20010315 (release)"

:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), "3" ((char *) (addr)),"2" (__KERNEL_CS << 16));

input으로 정의된 것 들이다. "3", "2"는 각각 %3(__d0), %2(__d1)로 대응되도록 한다. $APP 전의 3줄 중 윗 2줄이 "3", "2"에 해당하는 것들이다.

:"=m" (*((long *) (gate_addr))), "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1)

output으로 정의된 것 들. %0은 값이 0이되고(main에서 _set_gate(0, 1, 2, 3)으로 했기 때문에) %1은 4가 된다.