
$ 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
어셈블리어를 잘 모르는 사람들을 위해 위 코드에 등장하는 명령어들을 간단히 살펴보자.
| C declaration | Intel data type | Assembly-code suffix | Size(bytes) |
|---|---|---|---|
| char | Byte | b | 1 |
| short | Word | w | 2 |
| int | Double word | l | 4 |
| long | Quad word | q | 8 |
| char * | Quad word | q | 8 |
| float | Single precision | s | 4 |
| double | Double precision | l | 8 |
r로 시작(%rax, %rsp)e로 시작(%eax, %esp)%ax, %bx, %si, %sp)l로 끝남(%al, %spl)
int f(int a, int b, int c);
이런 식으로 함수를 호출하면,
a) : %rdib) : %rsic) : %rdx%rcx%r8%r9일곱번째부터는 레지스터가 아니라 스택을 사용하여 전달한다.
mov S, D : D <- S
movl $0, %edx : edx 레지스터의 값을 0으로 만든다. (%edx는 32비트(Double word)이므로, 접미사 l을 붙여야 한다.movslq %edx, %rcx : rcx 레지스터에 edx 레지스터의 값을 대입한다. l과 q는 각각 edx 레지스터와 rcx 레지스터의 크기다.movz와 movs가 있다. movz는 무조건 0으로 채워넣는 것이며, movs는 최상위 부호 비트를 연장하여 부호를 유지하는 것이다. add S, D : D <- S + D
addl $1, %edx : edx 레지스터에 1을 더해 edx 레지스터에 덮어쓴다. addl (%rdi,%rcx,4), %eax : eax 레지스터에 *(%rdi + %rcx*4) 값을 더해 eax레지스터에 덮어쓴다. (여기서 %rdi는 배열의 주소, %rcx는 배열의 인덱스, 4는 자료형 기본 바이트 수에 대응된다.)jmp Label : Label에 있는 값을 다음 인스트럭션의 주소로 사용한다.
jmp .L2 : .L2로 이동cmpl %esi, %edx; jl .L3cmp S1, S2 : S2 - S1 연산을 하여 조건 코드 설정 jl .L3 : 조건 코드를 확인한 결과 l(less)이면, .L3으로 점프(조건부 점프)%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가 왜 저수준 언어가 아닌지 의문을 가졌다면, 어셈블리어를 보고 단번에 깨달을 수 있다. 이정도로 컴퓨터한테 떠먹여줘야 저수준 언어인 것이다!