티스토리 뷰
*본 글은 https://www.programering.com/a/MjMycDMwATQ.html의 포스팅을 번역 한 뒤
직접 실험하여 부가설명을 제가 붙였습니다. 이상한 부분은 지적 부탁드리겠습니다.
printf의 숨겨진 뒷 이야기.
프로그래밍 언어를 말할 때, C언어에서 아마도 제일 간단하고 많이 알려진 코드는 Hello World일 것 입니다. printf라는 간단한 함수는 그 자체로 완벽하며 명료합니다. 하지만 그 뒤에서는 무슨일이 일어나고 있을까요? 아마 대부분 사람들은 이를 신경쓰지 않을것입니다. 그래서 우린 이 뒷이야기를 한번 논의해 보려합니다.
빨간배경은 명령어 라인이며 파란배경은 소스 코드 혹은 코드의 산출물에 해당합니다.( o,as 파일 등)
아래의 소스코드는 컴파일러를 통해 운영체제가 Hello World를 올바르게 표현해냅니다.
이러기 위해 컴파일러는 전처리 과정을 포함하여 컴파일을 하고 그러고 난뒤 어셈블리로 표현하게 되고 마지막으로 링크되는 과정까지 하여 총 네개의 과정을 거치게 됩니다.
#include<stdio.h>
int main()
{
printf("Hello World !\n");
return 0;
}
첫 단계는 전처리기가 소스코드내 매크로를 처리하는데 가령 #include가 이에 해당합니다. 전처리기가 모두 완료되면 전처리된 소스에서 printf함수의 정의를 볼 수 있게 됩니다.
다음 수행을 위해 자신의 리눅스에 포함되어 있는 gcc버전의 디렉토리로 이동하여 cc1 파일을 이용해 i파일을 산출합니다. i파일은 전처리기와 컴파일의 중간단계 파일로써 #define과 #include를 확장하고 #if이 결과에 맞게 산출한 결과물입니다.
$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -E -quiet main.c -o main.i
(일부 헤더파일이 없어 제대로 변환이 안될 수 있습니다. 이때는 해당 헤더파일을 검사해서 어떤걸 추가로 install해야하는지 알 수 있으며 저의 경우에는 sudo apt-get install gcc-multilib g++-multilib 를 통해 해결하였습니다.)
# 1 "main.c"
# 1 "<The command line>"
# 1 "main.c"
...
extern int printf (const char *__restrict __format, ...);
...
int main()
{
printf("Hello World!\n");
return 0;
}
그런 다음, gcc툴킷인 cc1을 이용하여 전처리된 소스를 어셈블리어로 변환시킵니다.
$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet main.i -o main.s
.file "main.c"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
leave
ret
.size main, .-main
...
이제 어셈블리 문장에서 printf가 printf를 호출하는것이 아닌 puts를 호출하는 명령어로 변환되었음을 볼 수 있습니다. 이와 같은 현상이 일어나는 이유는 컴파일러의 최적화에 따른 현상입니다.
이 현상이 우리에게 보여주는것은, printf의 매개변수로 순수하게 \n로 끝나는 스트링(리터럴 문자열)인 경우 printf는 puts를 호출할 수 있게 최적화 됩니다. 다음으로 \n으로 끝나는 문자열 제거되고 마지막으론 일반적인 printf함수를 호출하게 됩니다.
*어셈블리에서 .LC0은 상수로컬변수를 의미합니다. call puts직전에 .LC0을 참조하는 것을 알 수있습니다.)
만일 계속하여 "Hello World! \n" 리터럴 문자열을 printf를 통해 처리하려 하면 호출 즉시 output buffer를 비워내기 때문에 printf로 문자열 매개변수를 호출하려들면 안됩니다.
( *다시말해 printf함수만으로는 내부과정을 정확히 알 수 없게 됩니다.)
이런 이유로 우리는 설명을 위하여 계속 puts 함수를 통해 설명할 것 입니다.
.section .rodata
.LC0:
.string "hello world!\n"
...
call printf
...
다음 어셈블러가(명령어 as) 하는 일은 곧 바로 읽을 수 없는 이진형태의 목적파일을 컴파일 하는것 입니다. 아래 코드를 보기 위해서는 objdump라는 GCC 툴킷을 이용해하면 볼 수 있습니다. 하지만 이것만 가지고는 유효한 puts의 심볼릭 주소를 알 수 없습니다.
*심볼릭 주소 : 절대 경로 혹은 상대 경로로된 주소로서 다른 파일, 디렉터리를 참조할 수 있는 특별한 종류의 파일. 윈도우의 바로가기와 비슷한 개념.
$as -o main.o main.s
$objdump –d main.o
main.o: File format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 83 ec 10 sub $0x10,%esp
9: c7 04 24 00 00 00 00 movl $0x0,(%esp)
10: e8 fc ff ff ff call 11 <main+0x11>
15: b8 00 00 00 00 mov $0x0,%eax
1a: c9 leave
1b: c3 ret
링커는 즉시 심볼릭 puts주소를 올바르게 잡아줍니다. . 아래 코드는 printf를 호출하는데 있어 일어나는 일을 상세하게 볼 수 있으며 설명을 위해 동적링크에 대해 기술할것입니다.
(파일 위치는 gcc관련 파일들은 gcc --print-file-name=libc.a 를 통해 알 수 있습니다)
$/usr/lib/gcc/i686-linux-gnu/4.7/collect2 \
-static -o main \
/usr/lib/i386-linux-gnu/crt1.o \
/usr/lib/i386-linux-gnu/crti.o \
/usr/lib/gcc/i686-linux-gnu/4.7/crtbeginT.o \
main.o \
--start-group \
/usr/lib/gcc/i686-linux-gnu/4.7/libgcc.a \
/usr/lib/gcc/i686-linux-gnu/4.7/libgcc_eh.a \
/usr/lib/i386-linux-gnu/libc.a \
--end-group \
/usr/lib/gcc/i686-linux-gnu/4.7/crtend.o \
/usr/lib/i386-linux-gnu/crtn.o
$objdump –sd main
Disassembly of section .text:
...
08048ea4 <main>:
8048ea4: 55 push %ebp
8048ea5: 89 e5 mov %esp,%ebp
8048ea7: 83 e4 f0 and $0xfffffff0,%esp
8048eaa: 83 ec 10 sub $0x10,%esp
8048ead: c7 04 24 e8 86 0c 08 movl $0x80c86e8,(%esp)
8048eb4: e8 57 0a 00 00 call 8049910 <_IO_puts>
8048eb9: b8 00 00 00 00 mov $0x0,%eax
8048ebe: c9 leave
8048ebf: c3 ret
...
static link 단계에서는 링커가 CRT 목적파일들을 실행파일에 넣는 작업을 수행합니다.
* CRT 목적 파일들이란 개발자가 산출한 목적 파일들과 라이브러리 파일들의 초기화 과정을 위한 목적파일입니다. 다시말해 main함수 이전에 필요한 사전작업들을 위함이며 구체적인 동작은 컴파일러, 아키텍쳐마다 달리 동작합니다. CRT 목적파일에는 crt1.o, crti.o, crtbeginT.o, crtend.o, crtn.o의 핵심 목적파일이 포함됩니다.
(https://ko.wikipedia.org/wiki/Crt0)
위의 어셈블리 코드 수준에서 다시 설명하자면 puts함수는 ibc.a, libc.a, libgcc.a. libgcc_eh.a 의 어셈블리 파일을 필요로 하며 이 파일들은 다시 start-group과 end-group을 필요로 합니다.
(https://stackoverflow.com/questions/5651869/what-are-the-start-group-and-end-group-command-line-options -- start group과 end gorup에 관한 설명)
링크가 이루어 지고 나면, 올바른 puts주소를 호출하게 됩니다. 하지만 puts대신 어셈블리어에서 _IO_puts를 대신 호출하는 것을 볼 수 있습니다. 이는 잘못된것이 아닙니다. 이 문제는 아래의 readelf 명령어를 사용하여 메인 심볼 테이블을 확인하면 알 수 있습니다. 심볼 테이블을 보면 알 수 있듯이 puts와 _IO_puts의 심볼이 같음을 알 수 있습니다. objdump 명령어로는 오직 _IO_puts만아 출력 됩니다.
$readelf main –s
Symbol table '.symtab' contains 2307 entries:
Num: Value Size Type Bind Vis Ndx Name
...
1345: 08049910 352 FUNC WEAK DEFAULT 6 puts
...
1674: 08049910 352 FUNC GLOBAL DEFAULT 6 _IO_puts
...
그러면 puts함수 정의가 정말 libc.a에 있다고 확신할 수 있을까요? libc.a까 만들어지면 다음은 전역 심볼인 _IO_puts가 대신하며 ioputs.o의 결과물을 찾아야합니다. 이를 위해 심볼 테이블을 확인해보면 ioputs.o가 puts와 _IO_puts 심볼을 정의하는것을 확인할 수 있습니다. 이는 libc 라이브러리의 ioputs.o가 puts함수를 위해 있음을 알 수 있게 됩니다.
*libc에 관한설명은 https://stackoverflow.com/questions/11372872/what-the-role-of-libcglibc-in-our-linux-app / https://github.com/gnutel/embedded/wiki/Glibc-%EB%9E%80%3F 를 확인 바랍니다. 요약하자면 standard c libray를 의미합니다.
2편에 계속....
'프로그래밍 > C C++' 카테고리의 다른 글
boost singleton, noncopayble 적용하기. (0) | 2019.02.24 |
---|---|
The story behind printf 2편 (0) | 2019.01.27 |
gcc와 msvc에서 range-based for loop(범위 기반 반복문)사용의 주의점. (0) | 2018.05.22 |
Item20 : Specify comparsion types for associative conatainers of poniters (0) | 2018.05.13 |
Item 19: Understand the difference between equality and equivalence (0) | 2018.04.17 |
- Total
- Today
- Yesterday
- 피아노
- Spring
- STL
- C language
- 이루마
- 중국
- 여행
- yiruma
- Codejam
- 사천
- 드럼
- C++
- regex
- 코드잼
- 카카오 공채
- 악보
- Algorithm
- 정규표현식
- 중국여행
- cpp
- compile
- 문자열
- printf
- kernerl
- linux
- link
- 알고리즘
- peram jam
- Pointer
- python
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |