[C] C언어 컴파일 2/4단계: 컴파일러(Compiler)

Yongjun Park·2022년 1월 24일
0
post-thumbnail

커맨드

$ cc1 main.i -Og -o main.s
$ cc1 sum.i -Og -o sum.s

cc1 = C Compiler
cc1plus = C++ Compiler

cc1 프로그램이 없어서 실행을 하지 못한다면, 다음과 같은 명령어도 사용 가능하다.

$ gcc -S main.i -Og -o main.s
$ gcc -S sum.i -Og -o sum.s

gcc -S 옵션, -Og 플래그에 대한 자세한 내용은 gcc 기본 옵션 정리를 참고하시기 바랍니다.

소스파일에서 실행파일을 만드는 전 과정 역시 컴파일이라고 하지만, 이 글에서는 전처리가 끝난 .i C 파일을 .s 확장자인 어셈블리어 파일로 변경하는 단계, 즉 좁은 의미의 컴파일에 대해 설명한다.


.s 파일의 구성

위의 명령어를 통해 산출된 sum.s의 내용은 다음과 같다.

	.file	"sum.c"
	.text
	.globl	sum
	.type	sum, @function
sum:
.LFB0:
	.cfi_startproc
	movl	$0, %edx
	movl	$0, %eax
	jmp	.L2
.L3:
	movslq	%edx, %rcx
	addl	(%rdi,%rcx,4), %eax
	addl	$1, %edx
.L2:
	cmpl	%esi, %edx
	jl	.L3
	rep ret
	.cfi_endproc
.LFE0:
	.size	sum, .-sum
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

덜 중요한 부분은 빼고, 나머지만 나눠서 살펴보자.

	.file	"sum.c"
	.text
	.globl	sum
	.type	sum, @function
  • sum.c 라는 파일을 어셈블한 결과다.
  • sum이라는 심볼은 함수를 의미하며, 전역 심볼로서 다른 파일과 함께 링크 시 그 다른 파일에서도 이 심볼을 사용할 수 있다.
sum:
	movl	$0, %edx
	movl	$0, %eax
	jmp	.L2
.L3:
	movslq	%edx, %rcx
	addl	(%rdi,%rcx,4), %eax
	addl	$1, %edx
.L2:
	cmpl	%esi, %edx
	jl	.L3
	rep ret

어셈블리어를 잘 모르는 사람들을 위해 위 코드에 등장하는 명령어들을 간단히 살펴보자.


어셈블리어 기초

사전 지식 1. 자료형의 길이를 나타내는 접미사(suffix)(C 기준)

C declarationIntel data typeAssembly-code suffixSize(bytes)
charByteb1
shortWordw2
intDouble wordl4
longQuad wordq8
char *Quad wordq8
floatSingle precisions4
doubleDouble precisionl8

사전 지식 2. 크기별 레지스터

  1. 64비트(Quad word, 접미사 q) : r로 시작(%rax, %rsp)
  2. 32비트(Double word, 접미사 l) : e로 시작(%eax, %esp)
  3. 16비트(Word, 접미사 w) : 규칙 없음(%ax, %bx, %si, %sp)
  4. 8비트(Byte, 접미사 b) : l로 끝남(%al, %spl)

사전 지식 3. 함수 인자 레지스터

int f(int a, int b, int c);

이런 식으로 함수를 호출하면,

  1. 첫번째 인자(a) : %rdi
  2. 두번째 인자(b) : %rsi
  3. 세번째 인자(c) : %rdx
  4. 네번째 인자 : %rcx
  5. 다섯번째 인자 : %r8
  6. 여섯번째 인자 : %r9

일곱번째부터는 레지스터가 아니라 스택을 사용하여 전달한다.


코드 이해

mov S, D : D <- S

  1. movl $0, %edx : edx 레지스터의 값을 0으로 만든다. (%edx는 32비트(Double word)이므로, 접미사 l을 붙여야 한다.
  2. movslq %edx, %rcx : rcx 레지스터에 edx 레지스터의 값을 대입한다. lq는 각각 edx 레지스터와 rcx 레지스터의 크기다.
    크기가 작은 레지스터에서 큰 레지스터로 값을 대입하면 상위 비트가 남는데, 그것을 처리하는 방법에는 두가지, movzmovs가 있다. movz는 무조건 0으로 채워넣는 것이며, movs는 최상위 부호 비트를 연장하여 부호를 유지하는 것이다.

add S, D : D <- S + D

  1. addl $1, %edx : edx 레지스터에 1을 더해 edx 레지스터에 덮어쓴다.
  2. addl (%rdi,%rcx,4), %eax : eax 레지스터에 *(%rdi + %rcx*4) 값을 더해 eax레지스터에 덮어쓴다. (여기서 %rdi는 배열의 주소, %rcx는 배열의 인덱스, 4는 자료형 기본 바이트 수에 대응된다.)

jmp Label : Label에 있는 값을 다음 인스트럭션의 주소로 사용한다.

  1. jmp .L2 : .L2로 이동
  2. cmpl %esi, %edx; jl .L3
    1. cmp S1, S2 : S2 - S1 연산을 하여 조건 코드 설정
    2. jl .L3 : 조건 코드를 확인한 결과 l(less)이면, .L3으로 점프(조건부 점프)
    3. 종합 : %edx%esi보다 작으면 .L3로 점프한다.

ret : C의 return과 동일. %eax에 리턴 값을 넣는다.


// sum.c
int sum(int *a, int n)
{
	int s = 0;

	for (int i = 0; i < n; i++) {
		s += a[i];
	}
	return s;
}

sum.s 파일 우측에, 대응되는 sum.c 코드를 주석으로 써보았다.

sum:						; int *a -> %rdi, int n -> %rsi
	movl	$0, %edx			; int i = 0
	movl	$0, %eax			; int s = 0
	jmp	.L2
.L3:
	movslq	%edx, %rcx
	addl	(%rdi,%rcx,4), %eax		; s += *(a + i*4)
    						; s += a[i]와 동일한 의미
	addl	$1, %edx			; i++
.L2:
	cmpl	%esi, %edx			; if (i < n)
	jl	.L3				; loop 계속 돌기
	rep ret					; 아니라면 리턴(%rax에는 s가 저장되어 있음)
    						; 그러므로 return s와 동일한 의미

그동안 C가 왜 저수준 언어가 아닌지 의문을 가졌다면, 어셈블리어를 보고 단번에 깨달을 수 있다. 이정도로 컴퓨터한테 떠먹여줘야 저수준 언어인 것이다!

profile
추상화되었던 기술을 밑단까지 이해했을 때의 쾌감을 잊지 못합니다

0개의 댓글