$ 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
) : %rdi
b
) : %rsi
c
) : %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 .L3
cmp 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가 왜 저수준 언어가 아닌지 의문을 가졌다면, 어셈블리어를 보고 단번에 깨달을 수 있다. 이정도로 컴퓨터한테 떠먹여줘야 저수준 언어인 것이다!