[C] 시작
01
보통... 시작이라는 건 어떠한가요?
아마도 콘솔에 "Hello, World!"를 출력하는 것으로부터 첫 발을 떼게 되는 것 같습니다.
최근에 C를 기초부터 복습하는 시간을 가졌습니다. hello world의 출력을 기대하며 사뿐한 시작을 할 것으로 예상했습니다만... 개인적으로 인상적인 출발과 함께 복습이 시작되었습니다.
그 때의 복습 내용에 더해서 제가 학습에 보탠 것들을 정리해서 적어보고자 합니다.
아무래도 내용은 C를 처음 배우는 사람을 위한 것은 아닐 것 같습니다. 시작해볼까요잉
우선 gcc 를 설치해봅니다. 환경은 윈도우 11입니다.
저는 msys2(https://www.msys2.org/) 를 통해 gcc를 비롯한 여러 도구들을 설치했습니다.
그리고 아무런 내용도 적혀 있지 않은 파일 hello.c 를 만들었습니다.
우리는 gcc 등 어쩌고 저쩌고를 통해 코드를 실행 파일로 만들 수 있다는 사실을 알고 있습니다.
시작해보겠습니다. gcc 로 어떤 내용도 적혀 있지 않은 hello.c 를 어떻게든 해보세요!! 라고 명령하겠습니다.
gcc hello.c
그 결과는 다음과 같이 출력됩니다.
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-crtexewin.o): in function `main':
D:/W/B/src/mingw-w64/mingw-w64-crt/crt/crtexewin.c:66:(.text.startup+0xb5): undefined reference to `WinMain'
collect2.exe: error: ld returned 1 exit status이 출력에 대해 대충 생각해봅시다
- gcc.exe를 통해 hello.c를 실행 파일로 만드려고 했음
- collect2.exe가 에러를 뱉었음
- 보니까 ld.exe가 실행되었는데, 얘가
libmingw32.a와lib64_libmingw32_a-crtexewin.o를 얘기하고 있음 - 그 얘기 안에서,
crtexewin.c의 66번째 줄과0xb5?가 언급되고 있음 - 거기서 WinMain이라는 것이 'undefined reference'라고 말함
정말 대충이죠... 더 대충 생각하면서 멍청하게 읽어서 다음 단계를 시도해봅니다.
- 'WinMain'에 대해 작성하고
gcc hello.c를 실행시키기
빈 파일에 적어봅니다.
WinMain
이 상태로 gcc hello.c 를 실행시키면, 그 출력은 다음과 같습니다.
hello.c:1:1: error: expected '=', ',', ';', 'asm' or '__attribute__' at end of input
1 | WinMain
| ^~~~~~~이걸 또 읽어보면,
- hello.c의 1행에서 오류가 있다
- 그것은...
- '
=' - '
,' - '
;' - '
asm' - '
attribute' - 를, 파일 끝에서 기대하고 있었다
라고 말합니다.
그렇죠... 그냥 WinMain만을 적을 수는 없겠지요. 오류가 말하는 것에서의 기대를 충족시키면 될 것 같습니다.
- 'WinMain'을 더 자세하게 작성하고
gcc hello.c를 실행시키기
WinMain = 1;
결과는 다음과 같습니다.
hello.c:1:1: warning: data definition has no type or storage class
1 | WinMain = 1;
| ^~~~~~~
hello.c:1:1: error: type defaults to 'int' in declaration of 'WinMain' [-Wimplicit-int]- 'data definition'을 하는데 'type' 또는 'storage class'가 없다는 경고가 발생함
- 'WinMain'의 'declaration'에서 기본적으로 'int'로 'type'을 만들었다는 오류가 발생함
여기서 -Wimplicit-int 는 C의 표준과 관련된 내용이죠. 하지만 다른 방법을 시도해봅니다.
- 'WinMain'을 다르게 다시 작성하고
gcc hello.c를 실행시키기
int WinMain = 1;
위 결과에서 기본적으로 'int'라는 타입을 지정하고 어쩌고... 했습니다. 그래서 WinMain의 앞에 int를 붙여주었어요.
여기까지의 과정을 정리해보면, WinMain이 없다고 하는 것 같아서! WinMain을 적었는데 기대하는 것들이 있어서 그것들을 충족하고 있던 것입니다. 현재 단계까지의 결과는 'int' 타입의 WinMain에 '1'을 '할당'한 것과 같습니다.
이런 작업을 변수를 선언했다고 하잖아요?
변수가 뭔데요?


우리가 'WinMain'을 하나의 '값'으로 만든 거죠.
그래서 WinMain을 변수로 설정하면 그 결과는 어떻게 될까요?


실행 파일이 만들어졌습니다... 실행해보면, 아무런 출력도 오류도 없이 자동으로 종료되는 모습을 볼 수 있습니다.
그러면 잘 된 거 아냐? 과연 그럴까요?
이 때 GDB라는 도구를 한 번 사용해보겠습니다.

'실행을 추적' 할 수 있다! 한 번 추적해봅시다.
다음과 같이 GDB를 실행시켜봅니다.
gdb a.exeGNU gdb (GDB) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.exe...
(gdb) runrun을 해 주면, a.exe를 실행시키며 추적할 수 있습니다. 그리고,
Starting program: I:\a.exe
[New Thread 24752.0x6c88]
Thread 1 received signal SIGSEGV, Segmentation fault.
0x00007ff762923000 in __data_start__ ()SIGSEGV 신호를 받았다고 합니다. 이 SIGSEGV는 바로 뒤에 적힌 Segmentation fault에 대한 오류 신호입니다.
아까 실행한 a.exe 는 정상적으로 작동되고 종료된 것이 아니었던 것입니다! 이걸 GDB라는 도구를 통해서 확인할 수 있었습니다.
그런데... Segmentation fault는 뭘까요?



좋습니다... 요약해보면
- 프로그램이 허용되지 않은 메모리 영역에 접근을 시도
- 허용되지 않은 방법으로 메모리 영역에 접근을 시도
- 읽기 전용 영역에 쓰기를 시도
- 운영 체제에서 사용하는 영역에 다른 내용을 덮어쓰려 하는 경우
등등의 상황에 이러한 오류가 발생하는 것 같습니다.
그러면 정확히 어떠한 과정에서 이러한 일이 일어난 걸까요?
우리는 'WinMain'을 'int'라는 타입으로, '1'을 할당했습니다.
그리고 GDB를 통해 오류를 확인했습니다. 그 오류는 다음과 같았습니다.
Thread 1 received signal SIGSEGV, Segmentation fault.
0x00007ff762923000 in __data_start__ ()여기서 0x00007ff762923000 는 오류가 발생한 메모리 주소입니다.
그러면 __data_start__ 는 뭘까요?
이것은 링커에서 찾아볼 수 있습니다.
링커가 뭔데요? 자세한 설명은 나중에 하고... 코드를 실행 파일로 만드는 과정에서 아무튼간에 불려와서 일 하는 놈인데요. 이 링커가 답을 알고 있습니다.
ld --verbose 명령어를 실행하면 이 링커가 무엇을 하는지 자세히 확인해볼 수 있습니다.
.data BLOCK(__section_alignment__) :
{
__data_start__ = . ;
*(.data)
*(.data2)
*(SORT(.data$*))
KEEP(*(.jcr))
__data_end__ = . ;
*(.data_cygwin_nocopy)
}길고 긴 출력에서 발췌함
제가 찾던 data_start 여기 있네요 여기 있을 것 같았다. 그건 실로 벅찬 감격이었다.고마워요 제임스.
이것은 'data_start'라는 '심볼'을 현재 위치로 정의하고, .data 섹션에 대한 시작 지점을 만드는 것입니다.

.data 섹션이 뭔데요...이건 말이죠...


실행 파일은 이러한 형식을 따릅니다. 이러한 형식 내에서는 '섹션'이 존재합니다.
그 섹션은 .data , .text , .bss 등으로 나누어지고, 실행 파일 즉, 프로그램이 메모리에 로드될 때에 무엇이 어떻게 올라가는가에 따라 나누어지는 형태인 것입니다.
그게 뭔데요... 일단 오류가 발생한 'data_start' 지점은 이것과 관련이 있다는 것입니다.
그러면, nm 이라는 도구를 사용해봅시다.
nm 이 뭔데요...

대충 정리하면 코드를 가지고 만든 뭐시기들을 분석해서 그 내용을 볼 수 있는 도구라는 거에요...
nm a.exe | findstr "__data_start__"이렇게 해서 오류를 뱉어내는 우리의 실행파일에서 'data_start'에 대한 정보를 찾아봅시다.
0000000140003000 D __data_start__그러면 '0000000140003000' 이라는 주소에서 'D' 형태의 'data_start'가 발견됩니다. 이 때 'D'는 'data_start'라는 심볼이 'data' 섹션에 위치함을 의미합니다.
이번에는 'WinMain'도 찾아볼까요?
nm a.exe | findstr "WinMain"Powershell
이에 대한 출력은,
0000000140003000 D WinMain
00000001400013e0 T WinMainCRTStartup이와 같이 나왔습니다. 보이시나요?
0000000140003000 D __data_start__
0000000140003000 D WinMain'WinMain'과 'data_start'가 같은 주소에서 'data' 섹션에 위치하고 있습니다.
이번에는 objdump 를 사용해서 분석해보겠습니다.


이것 또한 대충 요약하면 코드로 만든 뭐시기들을 분석할 수 있는 도구입니다...
objdump -d a.exe | findstr "WinMain"Powershell
이 명령어를 통해 우리의 실행파일을 '어셈블리어'로 보이게 하고, 그 과정에서 'WinMain'이 포함된 곳을 찾아냅니다.
어셈블리어는 '기계어'에 대응되는 프로그래밍 언어입니다.
대충... 0과 1에 더 가까워지는 과정입니다
결과는 다음과 같습니다.
00000001400013e0 <WinMainCRTStartup>:
1400028d4: e9 27 07 00 00 jmp 140003000 <WinMain>
대충... 'WinMainCRTStartup' 이라는 '함수'가 '1400028d4'라는 주소에서 'jmp'라는 명령을 해서 '140003000' 주소로 가서 실행하쇼 라는 의미입니다. 그리고 거기에 'WinMain'이 보이구요.
함수란 뭘까요...

그렇습니다. 함수란 뭔가를 실행하는 것이고 그걸 해 주는 녀석의 이름이 'WinMainCRTStartup'이에요.
아까 전에
0000000140003000 D __data_start__
0000000140003000 D WinMain이러한 결과도 확인했었죠. jmp하는 주소인 '140003000'이 동일합니다.
그러면 정리해봅시다.
- 빈 코드를 넣었더니 WinMain을 달라고 하는 것 같다.
- 그래서 WinMain만 적어서 넣었더니 그건 내가 기대하는 것과 다르다고 한다.
- 그래서 기대하던대로 넣었더니 'type'이 문제가 있다고 해서 'int' 타입으로 정해서 '1'을 할당해서 넣었더니 실행 파일이 나왔다.
- 실행 파일이 제대로 실행이 되나 싶더니 분석 도구를 사용해서 살펴보면 문제가 있었다. 그 문제는 'data_start'에서 발생했다고 한다
- 다른 분석 도구를 사용해서 보니까 적어넣은 'WinMain'과 문제 있는 'data_start'의 주소가 같다?
- 또 다른 분석 도구를 사용해서 보니까 'WinMainCRTStartup'이 'WinMain'으로 점프하게 되어있다?
여기서 아까 전에 gdb를 통해서 확인했던 이 오류를 다시 봅시다.
Thread 1 received signal SIGSEGV, Segmentation fault.
0x00007ff762923000 in __data_start__ ()여기서 0x00007ff762923000 는 오류가 발생한 메모리 주소라고 했습니다.
그런데 'data_start'는 다른 분석 도구들을 사용해서 확인했을 때에 주소가 '140003000'라고 하지 않았나요? 왜 다른 값이 나오는 거죠?
잠시 gdb를 다시 실행시켜봅시다.
gdb a.exe
그리고 maintenance info sections 를 입력합니다. 그러면,
[0] 0x7ff762921000->0x7ff762922900 at 0x00000600: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
[1] 0x7ff762923000->0x7ff7629230b0 at 0x00002000: .data ALLOC LOAD DATA HAS_CONTENTS
[2] 0x7ff762924000->0x7ff762924be8 at 0x00002200: .rdata ALLOC LOAD READONLY DATA HAS_CONTENTS
[3] 0x7ff762925000->0x7ff762925204 at 0x00002e00: .pdata ALLOC LOAD READONLY DATA HAS_CONTENTS
[4] 0x7ff762926000->0x7ff762926198 at 0x00003200: .xdata ALLOC LOAD READONLY DATA HAS_CONTENTS
[5] 0x7ff762927000->0x7ff762927180 at 0x00000000: .bss ALLOC
[6] 0x7ff762928000->0x7ff7629288d0 at 0x00003400: .idata ALLOC LOAD READONLY DATA HAS_CONTENTS
[7] 0x7ff762929000->0x7ff762929010 at 0x00003e00: .tls ALLOC LOAD DATA HAS_CONTENTS
[8] 0x7ff76292a000->0x7ff76292a4e8 at 0x00004000: .rsrc ALLOC LOAD READONLY DATA HAS_CONTENTS
[9] 0x7ff76292b000->0x7ff76292b060 at 0x00004600: .reloc ALLOC LOAD READONLY DATA HAS_CONTENTS
[10] 0x14000c000->0x14000c4e0 at 0x00004800: .debug_aranges READONLY HAS_CONTENTS
[11] 0x14000d000->0x14001824d at 0x00004e00: .debug_info READONLY HAS_CONTENTS
[12] 0x140019000->0x14001b16b at 0x00010200: .debug_abbrev READONLY HAS_CONTENTS
[13] 0x14001c000->0x14001dea9 at 0x00012400: .debug_line READONLY HAS_CONTENTS
[14] 0x14001e000->0x14001e848 at 0x00014400: .debug_frame READONLY HAS_CONTENTS
[15] 0x14001f000->0x14001f2d2 at 0x00014e00: .debug_str READONLY HAS_CONTENTS
[16] 0x140020000->0x140021a3e at 0x00015200: .debug_line_str READONLY HAS_CONTENTS
[17] 0x140022000->0x14002330c at 0x00016e00: .debug_loclists READONLY HAS_CONTENTS
[18] 0x140024000->0x1400241c4 at 0x00018200: .debug_rnglists READONLY HAS_CONTENTS이러한 출력이 있습니다. 여기서,
[1] 0x7ff762923000->0x7ff7629230b0 at 0x00002000: .data ALLOC LOAD DATA HAS_CONTENTS 를 보면, .data 섹션이 오류가 발생한 0x00007ff762923000 과 일치합니다.
그 뒤에 적혀있는 ALLOC LOAD DATA HAS_CONTENTS 는 무엇을 뜻하는 걸까요?

이것은 gdb 공식 문서에서 설명하는 섹션 뒤에 붙는 플래그들입니다. 여기서 하나씩 읽어보면
- ALLOC : 섹션이 프로세스에서 할당되는 공간을 가질 것이라는 플래그
- LOAD : 섹션이 자식 프로세스 메모리로의 파일로부터 로드될 것이라는 플래그
- DATA : 섹션이 오직 'data'만 포함한다는 플래그(실행 가능한 코드는 없음)
- HAS_CONTENT : 섹션이 비어있지 않다는 플래그
여기서 다른 것들을 제치고 'DATA' 플래그만 보면, 이 섹션은 실행 가능한 코드를 포함할 수 없는 섹션이라는 것을 알게 됩니다.
그러니까, 오류가 발생한 '0x00007ff762923000' 는, 뭔가를 실행할 수가 없는 곳이라는 뜻이네요. 그런데 그게 어쨌다는 것인지?
다시, 아까 사용했던 objdump 를 사용해봅시다. 이번에는
objdump -p a.exe | findstr "Characteristics"
이에 대한 결과는 다음과 같이 출력됩니다.
Characteristics 0x26
DllCharacteristics 00000160여기서 'DllCharacteristics'가 '00000160' 이랍니다. 그게 뭔데요...
그것은 여기서 확인할 수 있습니다.

여기서 00000160, 즉 0160은 다음과 같이 만들어집니다.
- IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA(0x0020)
- IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(0x0040)
- IMAGE_DLLCHARACTERISTICS_NX_COMPAT(0x0100)

대충 요약하면,
- 실행 파일이 실행 될 때 메모리에 로드되는 주소를 잘 스까준다(ASLR).
- 데이터 영역에서 코드를 실행하는 것을 방지시켜준다(DEP).
의 기능을 적용할 수 있다는 것입니다. 우리의 실행파일에도 그것이 적용되어 있었다는 것이구요...

그러면 gdb를 통해 확인한 오류 주소 '0x00007ff762923000'는, 스까진 주소였던 것인가?
gcc hello.c "-Wl,--disable-dynamicbase" -o b.exe 를 통해 새로운 실행 파일을 만들어봅시다.
뭔가 길어졌죠? 여러 옵션들을 넣었습니다. 이번에는 실행파일이 b.exe 라는 이름으로 만들어지고, 링커(누구세요)에게 주소를 스까지 마세용 하고 주문했습니다.
gdb b.exe 를 아까와 같이 다시 시도하고 run 해 봅니다. 그러면,
Thread 1 received signal SIGSEGV, Segmentation fault.
0x0000000140003000 in __data_start__ ()0x00007ff762923000가 아까 확인했던 주소와 같은 140003000 으로 바뀌었습니다!
그러니까,
'WinMain'과 'data_start'의 위치는 동일하고, 그 위치에서 오류가 발생했다
라는 사실을 다시 한 번 확인했습니다.
'WinMain'은 'int'라는 'type'으로, '1'이라는 값으로 '할당' 하였다... 이것은 우리가 '변수'로 만들었다는 것을 알고 있습니다.
이게 왜 문제인데요?
gcc hello.c 를 통해 실행 파일을 만드려고 하는 과정을 조금 더 자세히 보겠습니다.
gcc -v hello.c 를 실행하면, 'verbose' 옵션이 활성화되어 뭔가를 막 더 쏟아냅니다.
Using built-in specs.
COLLECT_GCC=C:\msys64\ucrt64\bin\gcc.exe
COLLECT_LTO_WRAPPER=C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../gcc-15.2.0/configure --prefix=/ucrt64 --with-local-prefix=/ucrt64/local --with-native-system-header-dir=/ucrt64/include --libexecdir=/ucrt64/lib --enable-bootstrap --enable-checking=release --with-arch=nocona --with-tune=generic --enable-mingw-wildcard --enable-languages=c,lto,c++,fortran,ada,objc,obj-c++,jit --enable-shared --enable-static --enable-libatomic --enable-threads=posix --enable-graphite --enable-fully-dynamic-string --enable-libstdcxx-backtrace=yes --enable-libstdcxx-filesystem-ts --enable-libstdcxx-time --disable-libstdcxx-pch --enable-lto --enable-libgomp --disable-libssp --disable-multilib --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-libiconv --with-system-zlib --with-gmp=/ucrt64 --with-mpfr=/ucrt64 --with-mpc=/ucrt64 --with-isl=/ucrt64 --with-pkgversion='Rev8, Built by MSYS2 project' --with-bugurl=https://github.com/msys2/MINGW-packages/issues --with-gnu-as --with-gnu-ld --with-libstdcxx-zoneinfo=yes --disable-libstdcxx-debug --enable-plugin --with-boot-ldflags=-static-libstdc++ --with-stage1-ldflags=-static-libstdc++
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 15.2.0 (Rev8, Built by MSYS2 project)
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=nocona' '-dumpdir' 'a-'
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/cc1.exe -quiet -v -iprefix C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/ -D_REENTRANT hello.c -quiet -dumpdir a- -dumpbase hello.c -dumpbase-ext .c -mtune=generic -march=nocona -version -o C:\Users\lee\AppData\Local\Temp\ccYo1gPm.s
GNU C23 (Rev8, Built by MSYS2 project) version 15.2.0 (x86_64-w64-mingw32)
compiled by GNU C version 15.2.0, GMP version 6.3.0, MPFR version 4.2.2, MPC version 1.3.1, isl version isl-0.27-GMP
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/include"
ignoring duplicate directory "C:/msys64/ucrt64/lib/gcc/../../lib/gcc/x86_64-w64-mingw32/15.2.0/include"
ignoring nonexistent directory "D:/M/msys64/ucrt64/include"
ignoring nonexistent directory "/ucrt64/include"
ignoring duplicate directory "C:/msys64/ucrt64/lib/gcc/../../lib/gcc/x86_64-w64-mingw32/15.2.0/include-fixed"
ignoring nonexistent directory "C:/msys64/ucrt64/lib/gcc/../../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/include"
ignoring nonexistent directory "D:/M/msys64/ucrt64/include"
#include "..." search starts here:
#include <...> search starts here:
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/include
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../include
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/include-fixed
End of search list.
Compiler executable checksum: f68563dd6aebdc6b84233a210e98d3c3
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=nocona' '-dumpdir' 'a-'
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/as.exe -v -o C:\Users\lee\AppData\Local\Temp\ccot93fD.o C:\Users\lee\AppData\Local\Temp\ccYo1gPm.s
GNU assembler version 2.45 (x86_64-w64-mingw32) using BFD version (GNU Binutils) 2.45
COMPILER_PATH=C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/;C:/msys64/ucrt64/bin/../lib/gcc/;C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/
LIBRARY_PATH=C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/;C:/msys64/ucrt64/bin/../lib/gcc/;C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/lib/../lib/;C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/;C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/lib/;C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=nocona' '-dumpdir' 'a.'
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/collect2.exe -plugin C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/liblto_plugin.dll -plugin-opt=C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/lto-wrapper.exe -plugin-opt=-fresolution=C:\Users\lee\AppData\Local\Temp\ccgcf9jZ.res -plugin-opt=-pass-through=-lmingw32 -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lmingwex -plugin-opt=-pass-through=-lmsvcrt -plugin-opt=-pass-through=-lkernel32 -plugin-opt=-pass-through=-lpthread -plugin-opt=-pass-through=-ladvapi32 -plugin-opt=-pass-through=-lshell32 -plugin-opt=-pass-through=-luser32 -plugin-opt=-pass-through=-lkernel32 -plugin-opt=-pass-through=-lmingw32 -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lmingwex -plugin-opt=-pass-through=-lmsvcrt -plugin-opt=-pass-through=-lkernel32 -m i386pep -Bdynamic C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/crtbegin.o -LC:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0 -LC:/msys64/ucrt64/bin/../lib/gcc -LC:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/lib/../lib -LC:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib -LC:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/lib -LC:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../.. C:\Users\lee\AppData\Local\Temp\ccot93fD.o -lmingw32 -lgcc -lgcc_eh -lmingwex -lmsvcrt -lkernel32 -lpthread -ladvapi32 -lshell32 -luser32 -lkernel32 -lmingw32 -lgcc -lgcc_eh -lmingwex -lmsvcrt -lkernel32 C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/default-manifest.o C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/crtend.o
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=nocona' '-dumpdir' 'a.'
이 로그를 매직아이로 잘 보면, 이 녀석이 있습니다.
collect2.exe
가장 처음에, 아무런 내용도 작성하지 않고 명령을 했을 때에 이러한 오류가 있었죠.
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-crtexewin.o): in function `main':
D:/W/B/src/mingw-w64/mingw-w64-crt/crt/crtexewin.c:66:(.text.startup+0xb5): undefined reference to `WinMain'
collect2.exe: error: ld returned 1 exit status여기서 봤던 친구입니다. 이 친구는 링커를 호출하고 실행을 위한 각종 초기화 작업을 진행해줍니다.
아니 그래서 링커가 뭔데요? 그게 뭐냐면...



대충 정리하면, 링커는
- 실행 파일을 만드는 과정에서 여러 기능을 갖는 것들이 있음
- 그러한 기능이 꼭 통짜로 존재하는 것만은 아니니까 기능별로, 혹은 목적에 따라 소분되어있는데
- 소분된 것들을 모아서 잘 실행될 수 있도록 잘 배치해줍니다
그 소분된 것들이 뭐냐면,
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o
이런 '오브젝트 파일(*.o)' 들입니다.
아까 objdump를 통해서
objdump -d a.exe | findstr "WinMain"
00000001400013e0 <WinMainCRTStartup>:
1400028d4: e9 27 07 00 00 jmp 140003000 <WinMain>'WinMain'의 흔적을 보았습니다.
'WinMainCRTStartup'을 실행하는 과정에서, 'WinMain'으로 점프한다는 내용이었죠.
이번에는 crt2.o 를 뜯어보면 'WinMain'을 찾을 수 있을까요?
nm "C:/msys64/ucrt64/lib/crt2.o" | findstr "WinMain" 로 찾아봅시다.
00000000000003e0 T WinMainCRTStartup여기서 'T'는 .text 섹션에 있는 실행할 수 있는 코드를 의미합니다.
그러니까, 우리의 실행 파일에서의 'WinMainCRTStartup' 함수는 crt2.o에서 있던 것을 링커가 '끌고 왔다'고 볼 수 있는 것입니다.
그런데, 아까 텅 빈 코드의 오류에서
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-crtexewin.o): in function `main':
D:/W/B/src/mingw-w64/mingw-w64-crt/crt/crtexewin.c:66:(.text.startup+0xb5): undefined reference to `WinMain'
collect2.exe: error: ld returned 1 exit status'main' 함수에서 → 'WinMain'에 대한 오류가 있었다는 흐름이었죠.
맥락을 다시 보면,
- 실행 파일이 만들어진 지점에서는 'WinMainCRTStartup'이 'WinMain'으로 가게 함
- 텅 빈 코드를 보면 'main'에서 'WinMain'을 보고 있음

다시 objdump를 통해서 crt2.o 를 확인해봅시다.
objdump -d "C:/msys64/ucrt64/lib/crt2.o" 를 통해 crt2.o 를 어셈블리로 보겠습니다.
C:/msys64/ucrt64/lib/crt2.o: file format pe-x86-64
Disassembly of section .text:
0000000000000000 <__mingw_invalidParameterHandler>:
0: c3 ret
1: 0f 1f 40 00 nopl 0x0(%rax)
5: 66 66 2e 0f 1f 84 00 data16 cs nopw 0x0(%rax,%rax,1)
c: 00 00 00 00
0000000000000010 <__tmainCRTStartup>:
10: 41 57 push %r15
12: 41 56 push %r14
14: 41 55 push %r13
16: 41 54 push %r12
18: 55 push %rbp
19: 57 push %rdi
1a: 56 push %rsi
1b: 53 push %rbx
1c: 48 83 ec 58 sub $0x58,%rsp
20: b8 30 00 00 00 mov $0x30,%eax
25: 65 67 48 8b 00 mov %gs:(%eax),%rax
2a: 48 8b 70 08 mov 0x8(%rax),%rsi
2e: 48 8b 1d 00 00 00 00 mov 0x0(%rip),%rbx # 35 <__tmainCRTStartup+0x25>
35: 48 8b 3d 00 00 00 00 mov 0x0(%rip),%rdi # 3c <__tmainCRTStartup+0x2c>
(후략)
이 긴 출력에서 'WinMainCRTStartup'을 봅시다.
00000000000003e0 <WinMainCRTStartup>:
3e0: 48 83 ec 28 sub $0x28,%rsp아까 봤던 00000000000003e0 T WinMainCRTStartup 와 동일한 주소에서 일어나는 일은,
스택 포인터 레지스터에서 0x28 (40)을 빼서 스택 메모리 공간을 확보한다... 는 작업입니다.
그게 뭔데요... 잠시 눈 좀 비비고 이 아래 줄을 계속해서 보면
00000000000003e4 <.l_startw>:
3e4: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 3eb <.l_startw+0x7>
3eb: c7 00 01 00 00 00 movl $0x1,(%rax)
3f1: e8 1a fc ff ff call 10 <__tmainCRTStartup>
3f6: 90 nop여기서, 3f1: e8 1a fc ff ff call 10 <__tmainCRTStartup> 으로 이번에는 tmainCRTStartup 을 호출하고 있습니다.
이 10 에 해당하는 부분은 위에 있으니 위로 다시 올라가보겠습니다.
0000000000000010 <__tmainCRTStartup>:
10: 41 57 push %r15
12: 41 56 push %r14
14: 41 55 push %r13
16: 41 54 push %r12
18: 55 push %rbp
19: 57 push %rdi
1a: 56 push %rsi
1b: 53 push %rbx
1c: 48 83 ec 58 sub $0x58,%rsp
20: b8 30 00 00 00 mov $0x30,%eax
25: 65 67 48 8b 00 mov %gs:(%eax),%rax
2a: 48 8b 70 08 mov 0x8(%rax),%rsi
2e: 48 8b 1d 00 00 00 00 mov 0x0(%rip),%rbx # 35 <__tmainCRTStartup+0x25>
35: 48 8b 3d 00 00 00 00 mov 0x0(%rip),%rdi # 3c <__tmainCRTStartup+0x2c>
3c: eb 12 jmp 50 <__tmainCRTStartup+0x40>
3e: 66 90 xchg %ax,%ax
40: 48 39 c6 cmp %rax,%rsi
43: 0f 84 b7 00 00 00 je 100 <__tmainCRTStartup+0xf0>
49: b9 e8 03 00 00 mov $0x3e8,%ecx
4e: ff d7 call *%rdi
50: 31 c0 xor %eax,%eax
52: f0 48 0f b1 33 lock cmpxchg %rsi,(%rbx)
57: 75 e7 jne 40 <__tmainCRTStartup+0x30>
59: 45 31 f6 xor %r14d,%r14d
5c: 48 8b 2d 00 00 00 00 mov 0x0(%rip),%rbp # 63 <__tmainCRTStartup+0x53>
63: 8b 45 00 mov 0x0(%rbp),%eax
66: 83 f8 01 cmp $0x1,%eax
69: 0f 84 59 03 00 00 je 3c8 <__tmainCRTStartup+0x3b8>
6f: 8b 45 00 mov 0x0(%rbp),%eax
72: 85 c0 test %eax,%eax
74: 0f 84 96 00 00 00 je 110 <__tmainCRTStartup+0x100>
7a: c7 05 00 00 00 00 01 movl $0x1,0x0(%rip) # 84 <__tmainCRTStartup+0x74>
81: 00 00 00
84: 45 85 f6 test %r14d,%r14d
87: 0f 84 9b 02 00 00 je 328 <__tmainCRTStartup+0x318>
8d: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 94 <__tmainCRTStartup+0x84>
94: 48 8b 00 mov (%rax),%rax
97: 48 85 c0 test %rax,%rax
9a: 74 0c je a8 <__tmainCRTStartup+0x98>
9c: 45 31 c0 xor %r8d,%r8d
9f: ba 02 00 00 00 mov $0x2,%edx
a4: 31 c9 xor %ecx,%ecx
a6: ff d0 call *%rax
a8: e8 00 00 00 00 call ad <__tmainCRTStartup+0x9d>
ad: 4c 8b 05 10 00 00 00 mov 0x10(%rip),%r8 # c4 <__tmainCRTStartup+0xb4>
b4: 8b 0d 20 00 00 00 mov 0x20(%rip),%ecx # da <__tmainCRTStartup+0xca>
ba: 4c 89 00 mov %r8,(%rax)
bd: 48 8b 15 18 00 00 00 mov 0x18(%rip),%rdx # dc <__tmainCRTStartup+0xcc>
c4: e8 00 00 00 00 call c9 <__tmainCRTStartup+0xb9>
c9: 8b 0d 08 00 00 00 mov 0x8(%rip),%ecx # d7 <__tmainCRTStartup+0xc7>
cf: 85 c9 test %ecx,%ecx
d1: 0f 84 fb 02 00 00 je 3d2 <__tmainCRTStartup+0x3c2>
d7: 8b 15 04 00 00 00 mov 0x4(%rip),%edx # e1 <__tmainCRTStartup+0xd1>
dd: 85 d2 test %edx,%edx
df: 0f 84 2b 02 00 00 je 310 <__tmainCRTStartup+0x300>
e5: 48 83 c4 58 add $0x58,%rsp
e9: 5b pop %rbx
(후략)잠시... 잠시 다른 조사를 해 봅시다. crt2.o 에 대해서
objdump -r "C:/msys64/ucrt64/lib/crt2.o" 를 실행하면, 링커의 'relocation' 즉, 재배치 작업에 대한 정보를 볼 수 있습니다. 아까 링커는 여러 오브젝트 파일(여기서는 crt2.o 죠?)을 '배치' 해준다고 했었잖아요.
그 출력을 보면, 너무 기니까 이번에도 잘라서 보겠습니다.
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000031 IMAGE_REL_AMD64_REL32 .refptr.__native_startup_lock
0000000000000038 IMAGE_REL_AMD64_REL32 __imp_Sleep
000000000000005f IMAGE_REL_AMD64_REL32 .refptr.__native_startup_state
000000000000007c IMAGE_REL_AMD64_REL32 .bss
0000000000000090 IMAGE_REL_AMD64_REL32 .refptr.__dyn_tls_init_callback
00000000000000a9 IMAGE_REL_AMD64_REL32 __p___initenv
00000000000000b0 IMAGE_REL_AMD64_REL32 .bss
00000000000000b6 IMAGE_REL_AMD64_REL32 .bss
00000000000000c0 IMAGE_REL_AMD64_REL32 .bss
00000000000000c5 IMAGE_REL_AMD64_REL32 main
00000000000000cb IMAGE_REL_AMD64_REL32 .bss
00000000000000d9 IMAGE_REL_AMD64_REL32 .bss
0000000000000118 IMAGE_REL_AMD64_REL32 _pei386_runtime_relocator
000000000000011f IMAGE_REL_AMD64_REL32 _gnu_exception_handler
0000000000000125 IMAGE_REL_AMD64_REL32 __imp_SetUnhandledExceptionFilter
000000000000012c IMAGE_REL_AMD64_REL32 .refptr.__mingw_oldexcpt_handler
여기서, 00000000000000c5 IMAGE_REL_AMD64_REL32 main 를 보면, c5 오프셋에서 main 함수를 참조하고 있다는 것입니다. 그 타입은 IMAGE_REL_AMD64_REL32 이구요.
아까의 어셈블리에서 이 c5 근처를 한 번 봅시다.
bd: 48 8b 15 18 00 00 00 mov 0x18(%rip),%rdx # dc <__tmainCRTStartup+0xcc>
c4: e8 00 00 00 00 call c9 <__tmainCRTStartup+0xb9>
c9: 8b 0d 08 00 00 00 mov 0x8(%rip),%ecx # d7 <__tmainCRTStartup+0xc7>
c4 에서 1바이트의 opcode e8 (1바이트) 이 있고, 그 뒤에 00 00 00 00 이 나옵니다. 즉 c5 에서의 00000000000000c5 IMAGE_REL_AMD64_REL32 main 가 있었는데, 링커가 재배치를 할 때에 00 00 00 00 에 main 이 tmainCRTStartup 에 채워진다는 것입니다.
그 main이 어디서 올까요?
아까 빈 코드를 가지고 놀았을 때의 출력은 이랬습니다.
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-crtexewin.o): in function `main':
D:/W/B/src/mingw-w64/mingw-w64-crt/crt/crtexewin.c:66:(.text.startup+0xb5): undefined reference to `WinMain'
collect2.exe: error: ld returned 1 exit status이 구성을 다시 한 번 뜯어봅시다.
- ld.exe : 이건 링커입니다
- libmingw32.a - lib64_libmingw32_a-crtexewin.o : 그러면
- crtexewin.c : 이건 뭐죠
여기서 libmingw32.a 는 아카이브(*.a)입니다. 이 안에는 lib64_libmingw32_a-crtexewin.o 가 있고, 이 아카이브 파일에서 꺼내서 사용되는 것입니다.

네 사실입니다... 이를 위해 이번에는 ar 을 사용해봅시다.

앞선 gdb, objdump, nm 등을 사용하면서 -x 형식으로 뭔가를 추가 해주었었죠? 이것도 그런 방식의 사용이 가능합니다.

-t 옵션을 주면 아카이브의 내용물을 볼 수 있다고 합니다. 좋아용
ar -t "C:/msys64/ucrt64/lib/libmingw32.a" 를 사용해봅시다.
lib64_libmingw32_a-crtexewin.o
lib64_libmingw32_a-dll_argv.o
lib64_libmingw32_a-gccmain.o
lib64_libmingw32_a-natstart.o
lib64_libmingw32_a-pseudo-reloc-list.o
lib64_libmingw32_a-wildcard.o
lib64_libmingw32_a-charmax.o
lib64_libmingw32_a-ucrtexewin.o
lib64_libmingw32_a-dllargv.o
lib64_libmingw32_a-_newmode.o
lib64_libmingw32_a-tlssup.o
lib64_libmingw32_a-xncommod.o
lib64_libmingw32_a-cinitexe.o
lib64_libmingw32_a-merr.o
lib64_libmingw32_a-pesect.o
lib64_libmingw32_a-udllargc.o
lib64_libmingw32_a-xthdloc.o
lib64_libmingw32_a-mingw_custom.o
lib64_libmingw32_a-mingw_helpers.o
lib64_libmingw32_a-pseudo-reloc.o
lib64_libmingw32_a-udll_argv.o
lib64_libmingw32_a-usermatherr.o
lib64_libmingw32_a-xtxtmode.o
lib64_libmingw32_a-crt_handler.o
lib64_libmingw32_a-tlsthrd.o
lib64_libmingw32_a-tlsmthread.o
lib64_libmingw32_a-tlsmcrt.o
lib64_libmingw32_a-cxa_atexit.o
lib64_libmingw32_a-cxa_thread_atexit.o
lib64_libmingw32_a-tls_atexit.o
lib64_libmingw32_a-CRT_fp10.olibmingw32.a 에 포함된 오브젝트 파일들을 볼 수 있습니다.
여기서 잘 보면 제가 찾던 lib64_libmingw32_a-crtexewin.o 여기 있네요. 그건 실로 벅찬 감격이었다.고마워요 본드. 덕분에 마음이 아주 편해졌어요.고마워할 필요는 없어.킴은 미소지으며 손을 내밀었다.
그러니까 여기까지 오면 이제 한 가지 의문이 남습니다. libmingw32.a 에 lib64_libmingw32_a-crtexewin.o 가 포함이 되어있고, 링킹 과정에서 이를 찾아서 사용한 상황입니다. 그러면 출력에서 언급된 그 'main'이 여기서 와서 crt2.o 에 연결되는 걸까요?
우리의 지금까지의 흐름은 이렇습니다.
- 빈 코드를 넣었더니 WinMain을 달라고 하는 것 같다.
- 그래서 WinMain만 적어서 넣었더니 그건 내가 기대하는 것과 다르다고 한다.
- 그래서 기대하던대로 쫌 맞춰줬더니 'type'이 문제가 있다고 해서 'int' 타입으로 정해서 '1'을 할당해서 넣었더니 실행 파일이 나왔다.
- 실행 파일이 제대로 실행이 되나 싶더니 분석 도구를 사용해서 살펴보면 문제가 있었다. 그 문제는 'data_start'에서 발생했다고 한다
- 다른 분석 도구를 사용해서 보니까 적어넣은 'WinMain'과 문제 있는 'data_start'의 주소가 같다?
- 또 다른 분석 도구를 사용해서 보니까 'WinMainCRTStartup'이 'WinMain'으로 점프하게 되어있다?
- 그래서 일단
crt2.o를 멱살 잡고 보니까 여기서 'WinMainCRTStartup'을 부르는 것 같애 - 부르는 게 단순히 공간을 확보한 뒤에 'tmainCRTStartup'을 불러서 'main'으로 진입하고 있음
- main은 어디서 오는 거지??
main은,,, 어디서 오는 걸까요?
gcc hello.c "-Wl,-Map=build.map" 을 통해서 '링커 맵 파일'을 생성할 수 있습니다.
그게 뭔데요? 일단 실행한 뒤에 build.map 을 보시죠...
Archive member included to satisfy reference by file (symbol)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-crtexewin.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (main)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-gccmain.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (__main)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-natstart.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (__native_startup_lock)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-wildcard.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (_dowildcard)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-dllargv.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (_setargv)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-_newmode.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (_newmode)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-tlssup.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (__mingw_initltssuo_force)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-xncommod.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (_commode)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-cinitexe.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (__xc_z)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-merr.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (_matherr)
(후략)제목이 적혀있네요. 제목은 파일(심볼)에 의한 참조를 만족시키기 위한 아카이브 멤버 포함 내역입니다.
여기서 첫 줄부터 나옵니다.
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/libmingw32.a(lib64_libmingw32_a-crtexewin.o)
C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o (main)libmingw32.a 에서의 lib64_libmingw32_a-crtexewin.o 가 빌드에 포함되었고, 그것은 crt2.o 가 'main' 이라는 심볼을 필요로 했기 때문이다, 라고 적혀 있는 것입니다.
이렇게 의문이 해소되었습니다. 다시 정리해보면,
- 빈 코드를 넣었더니 WinMain을 달라고 하는 것 같다.
- 그래서 WinMain만 적어서 넣었더니 그건 내가 기대하는 것과 다르다고 한다.
- 그래서 기대하던대로 넣었더니 'type'이 문제가 있다고 해서 'int' 타입으로 정해서 '1'을 할당해서 넣었더니 실행 파일이 나왔다.
- 실행 파일이 제대로 실행이 되나 싶더니 분석 도구를 사용해서 살펴보면 문제가 있었다. 그 문제는 'data_start'에서 발생했다고 한다
- 다른 분석 도구를 사용해서 보니까 적어넣은 'WinMain'과 문제 있는 'data_start'의 주소가 같다?
- 또 다른 분석 도구를 사용해서 보니까 'WinMainCRTStartup'이 'WinMain'으로 점프하게 되어있다?
- 그래서 일단
crt2.o를 멱살 잡고 보니까 여기서 'WinMainCRTStartup'을 부르는 것 같애 - 거기서 'tmainCRTStartup'을 불러서 'main'으로 진입하고 있음
- main은 'WinMainCRTStartup'을 부르는
crt2.o가libmingw32.a의lib64_libmingw32_a-crtexewin.o에서 가져왔음 - ㅇㅋ
좋아요. 이제 build.map을 조금만 더 봅시다. 그러면,
.data 0x0000000140003000 0x200
0x0000000140003000 __data_start__ = .
*(.data)
.data 0x0000000140003000 0x0 C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../lib/crt2.o
.data 0x0000000140003000 0x0 C:/msys64/ucrt64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/crtbegin.o
.data 0x0000000140003000 0x10 C:\Users\lee\AppData\Local\Temp\cclIMjak.o
0x0000000140003000 WinMaindata_start 와 WinMain 이 같은 주소에 있는 것까지도 볼 수 있습니다. 그런데 이것의 근원은 C:\Users\lee\AppData\Local\Temp\cclIMjak.o 인데요, 제 성이 이씨고 사실 이건 기억에 오래 남기실 필요는 없습니다. 어쨌든 이 파일은 우리의 hello.c를 임시로 오브젝트 파일로 만든 후에 링커가 이것을 합치는 과정이 있었고 그 임시 오브젝트 파일에서 WinMain을 갖고 왔다는 것을 알 수 있는 부분입니다.
자, 그러면 이제 거의 다 온 것 같습니다.
ar -x "C:/msys64/ucrt64/lib/libmingw32.a" lib64_libmingw32_a-crtexewin.o 을 사용하면 아카이브에서 오브젝트 파일을 슬쩍 빼볼 수 있습니다. 그리고 나서 그 파일에 대해서,
objdump -d lib64_libmingw32_a-crtexewin.o 를 실행해서 어셈블리를 확인합니다.
lib64_libmingw32_a-crtexewin.o: file format pe-x86-64
Disassembly of section .text.startup:
0000000000000000 <main>:
0: 56 push %rsi
1: 53 push %rbx
2: 48 81 ec 98 00 00 00 sub $0x98,%rsp
9: 31 f6 xor %esi,%esi
b: e8 00 00 00 00 call 10 <main+0x10>
10: e8 00 00 00 00 call 15 <main+0x15>
15: 48 8b 18 mov (%rax),%rbx
18: 48 85 db test %rbx,%rbx
1b: 75 2e jne 4b <main+0x4b>
1d: e9 97 00 00 00 jmp b9 <main+0xb9>
22: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
28: 84 c9 test %cl,%cl
2a: 74 54 je 80 <main+0x80>
2c: 83 e6 01 and $0x1,%esi
2f: 74 2f je 60 <main+0x60>
31: be 01 00 00 00 mov $0x1,%esi
36: e8 00 00 00 00 call 3b <main+0x3b>
3b: 85 c0 test %eax,%eax
3d: 74 08 je 47 <main+0x47>
3f: 80 7b 01 01 cmpb $0x1,0x1(%rbx)
43: 48 83 db ff sbb $0xffffffffffffffff,%rbx
47: 48 83 c3 01 add $0x1,%rbx
4b: 0f be 0b movsbl (%rbx),%ecx
4e: 80 f9 20 cmp $0x20,%cl
51: 7e d5 jle 28 <main+0x28>
53: 89 f0 mov %esi,%eax
55: 83 f0 01 xor $0x1,%eax
58: 80 f9 22 cmp $0x22,%cl
5b: 0f 44 f0 cmove %eax,%esi
5e: eb d6 jmp 36 <main+0x36>
60: 84 c9 test %cl,%cl
62: 74 1c je 80 <main+0x80>
64: 90 nop
65: 66 66 2e 0f 1f 84 00 data16 cs nopw 0x0(%rax,%rax,1)
6c: 00 00 00 00
70: 0f b6 43 01 movzbl 0x1(%rbx),%eax
74: 48 83 c3 01 add $0x1,%rbx
78: 84 c0 test %al,%al
7a: 74 04 je 80 <main+0x80>
7c: 3c 20 cmp $0x20,%al
7e: 7e f0 jle 70 <main+0x70>
80: 48 8d 4c 24 20 lea 0x20(%rsp),%rcx
85: ff 15 00 00 00 00 call *0x0(%rip) # 8b <main+0x8b>
8b: 44 0f b7 4c 24 60 movzwl 0x60(%rsp),%r9d
91: 49 89 d8 mov %rbx,%r8
94: f6 44 24 5c 01 testb $0x1,0x5c(%rsp)
99: b8 0a 00 00 00 mov $0xa,%eax
9e: 48 8b 0d 00 00 00 00 mov 0x0(%rip),%rcx # a5 <main+0xa5>
a5: 44 0f 44 c8 cmove %eax,%r9d
a9: 31 d2 xor %edx,%edx
ab: 48 81 c4 98 00 00 00 add $0x98,%rsp
b2: 5b pop %rbx
b3: 5e pop %rsi
b4: e9 00 00 00 00 jmp b9 <main+0xb9>
b9: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx # c0 <main+0xc0>
c0: eb be jmp 80 <main+0x80>
c2: 90 nop
c3: 90 nop
c4: 90 nop
c5: 90 nop
c6: 90 nop
c7: 90 nop
c8: 90 nop
c9: 90 nop
ca: 90 nop
cb: 90 nop
cc: 90 nop
cd: 90 nop
ce: 90 nop
cf: 90 nop이 출력을 살펴보기 전에 잠시 한 번 더 진행해봅시다. objdump -r lib64_libmingw32_a-crtexewin.o 를 통해 relocation을 확인해봅니다.
lib64_libmingw32_a-crtexewin.o: file format pe-x86-64
RELOCATION RECORDS FOR [.text.startup]:
OFFSET TYPE VALUE
000000000000000c IMAGE_REL_AMD64_REL32 __main
0000000000000011 IMAGE_REL_AMD64_REL32 __p__acmdln
0000000000000037 IMAGE_REL_AMD64_REL32 _ismbblead
0000000000000087 IMAGE_REL_AMD64_REL32 __imp_GetStartupInfoA
00000000000000a1 IMAGE_REL_AMD64_REL32 .refptr.__ImageBase
00000000000000bc IMAGE_REL_AMD64_REL32 .rdata
00000000000000b5 IMAGE_REL_AMD64_REL32 WinMain
RELOCATION RECORDS FOR [.pdata.startup]:
OFFSET TYPE VALUE
0000000000000000 IMAGE_REL_AMD64_ADDR32NB .text.startup
0000000000000004 IMAGE_REL_AMD64_ADDR32NB .text.startup
0000000000000008 IMAGE_REL_AMD64_ADDR32NB .xdata.startup
RELOCATION RECORDS FOR [.debug_info]:
OFFSET TYPE VALUE
0000000000000008 IMAGE_REL_AMD64_SECREL .debug_abbrev
0000000000000068 IMAGE_REL_AMD64_SECREL .debug_line_str
000000000000006c IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000070 IMAGE_REL_AMD64_SECREL .debug_rnglists
000000000000007c IMAGE_REL_AMD64_SECREL .debug_line
0000000000000621 IMAGE_REL_AMD64_ADDR64 .text.startup
0000000000000642 IMAGE_REL_AMD64_SECREL .debug_loclists
0000000000000646 IMAGE_REL_AMD64_SECREL .debug_loclists
0000000000000655 IMAGE_REL_AMD64_SECREL .debug_loclists
0000000000000659 IMAGE_REL_AMD64_SECREL .debug_loclists
0000000000000668 IMAGE_REL_AMD64_SECREL .debug_loclists
000000000000066c IMAGE_REL_AMD64_SECREL .debug_loclists
0000000000000693 IMAGE_REL_AMD64_SECREL .debug_loclists
0000000000000697 IMAGE_REL_AMD64_SECREL .debug_loclists
00000000000006ab IMAGE_REL_AMD64_SECREL .debug_loclists
00000000000006af IMAGE_REL_AMD64_SECREL .debug_loclists
00000000000006b4 IMAGE_REL_AMD64_SECREL .debug_rnglists
00000000000006d1 IMAGE_REL_AMD64_SECREL .debug_loclists
00000000000006d5 IMAGE_REL_AMD64_SECREL .debug_loclists
00000000000006da IMAGE_REL_AMD64_ADDR64 .text.startup
00000000000006e8 IMAGE_REL_AMD64_SECREL .debug_rnglists
0000000000000709 IMAGE_REL_AMD64_ADDR64 .text.startup
000000000000071f IMAGE_REL_AMD64_ADDR64 .text.startup
000000000000072c IMAGE_REL_AMD64_ADDR64 .text.startup
0000000000000739 IMAGE_REL_AMD64_ADDR64 .text.startup
RELOCATION RECORDS FOR [.debug_loclists]:
OFFSET TYPE VALUE
0000000000000011 IMAGE_REL_AMD64_ADDR64 .text.startup
000000000000002d IMAGE_REL_AMD64_ADDR64 .text.startup
0000000000000049 IMAGE_REL_AMD64_ADDR64 .text.startup
0000000000000069 IMAGE_REL_AMD64_ADDR64 .text.startup
0000000000000091 IMAGE_REL_AMD64_ADDR64 .text.startup
00000000000000a4 IMAGE_REL_AMD64_ADDR64 .text.startup
RELOCATION RECORDS FOR [.debug_aranges]:
OFFSET TYPE VALUE
0000000000000006 IMAGE_REL_AMD64_SECREL .debug_info
0000000000000010 IMAGE_REL_AMD64_ADDR64 .text.startup
RELOCATION RECORDS FOR [.debug_rnglists]:
OFFSET TYPE VALUE
000000000000000d IMAGE_REL_AMD64_ADDR64 .text.startup
000000000000001d IMAGE_REL_AMD64_ADDR64 .text.startup
000000000000002d IMAGE_REL_AMD64_ADDR64 .text.startup
RELOCATION RECORDS FOR [.debug_line]:
OFFSET TYPE VALUE
0000000000000022 IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000026 IMAGE_REL_AMD64_SECREL .debug_line_str
000000000000002a IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000034 IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000039 IMAGE_REL_AMD64_SECREL .debug_line_str
000000000000003e IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000043 IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000048 IMAGE_REL_AMD64_SECREL .debug_line_str
000000000000004d IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000052 IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000057 IMAGE_REL_AMD64_SECREL .debug_line_str
000000000000005c IMAGE_REL_AMD64_SECREL .debug_line_str
0000000000000066 IMAGE_REL_AMD64_ADDR64 .text.startup
RELOCATION RECORDS FOR [.rdata$.refptr.__ImageBase]:
OFFSET TYPE VALUE
0000000000000000 IMAGE_REL_AMD64_ADDR64 __ImageBase
RELOCATION RECORDS FOR [.debug_frame]:
OFFSET TYPE VALUE
000000000000001c IMAGE_REL_AMD64_SECREL .debug_frame
0000000000000020 IMAGE_REL_AMD64_ADDR64 .text.startup00000000000000b5 IMAGE_REL_AMD64_REL32 WinMain 을 보면, 0xb5 오프셋에서 WinMain 심볼이 보입니다. 아까 막 뽑은 어셈블리를 한 번 보면,
b3: 5e pop %rsi
b4: e9 00 00 00 00 jmp b9 <main+0xb9>
b9: 48 8d 1d 00 00 00 00 lea 0x0(%rip),%rbx # c0 <main+0xc0>b4 주소에 e9 명령어(jmp)가 있고, 4바이트 placeholder가 있습니다. 이것이 이제 WinMain으로 가는 것입니다.
그러면 이제 알 것 같죠... main은 WinMain으로 향하는 것입니다. 또 다시 정리하면,
- 빈 코드를 넣었더니 WinMain을 달라고 하는 것 같다.
- 그래서 WinMain만 적어서 넣었더니 그건 내가 기대하는 것과 다르다고 한다.
- 그래서 기대하던대로 넣었더니 'type'이 문제가 있다고 해서 'int' 타입으로 정해서 '1'을 할당해서 넣었더니 실행 파일이 나왔다.
- 실행 파일이 제대로 실행이 되나 싶더니 분석 도구를 사용해서 살펴보면 문제가 있었다. 그 문제는 'data_start'에서 발생했다고 한다
- 다른 분석 도구를 사용해서 보니까 적어넣은 'WinMain'과 문제 있는 'data_start'의 주소가 같다?
- 또 다른 분석 도구를 사용해서 보니까 'WinMainCRTStartup'이 'WinMain'으로 점프하게 되어있다?
- 그래서 일단
crt2.o를 멱살 잡고 보니까 여기서 'WinMainCRTStartup'을 부르는 것 같애 - 부르는 게 단순히 공간을 확보한 뒤에 'tmainCRTStartup'을 불러서 'main'으로 진입하고 있음
- main은 'WinMainCRTStartup'을 부르는
crt2.o가libmingw32.a의lib64_libmingw32_a-crtexewin.o에서 가져왔음 lib64_libmingw32_a-crtexewin.o은 main에서 'WinMain' 으로 진입함
그러면 드디어 끝이 났습니다.
'WinMain'으로 진입하는 것은 어셈블리로 확인하였듯이 'jmp'로 실행하게 됩니다. 이 때 '실행' 할 곳에 진입한 프로그램은 코드를 실행할 수 없던 .data 섹션을 마주하고 말았습니다... 그렇게 이 프로그램은 SIGSEGV 신호를 보내며 뻗은 것이었습니다.
진짜... 진짜 마지막으로 정리하면,
- 빈 코드를 넣었더니 WinMain을 달라고 하는 것 같다.
- 그래서 WinMain만 적어서 넣었더니 그건 내가 기대하는 것과 다르다고 한다.
- 그래서 기대하던대로 넣었더니 'type'이 문제가 있다고 해서 'int' 타입으로 정해서 '1'을 할당해서 넣었더니 실행 파일이 나왔다.
- 실행 파일이 제대로 실행이 되나 싶더니 분석 도구를 사용해서 살펴보면 문제가 있었다. 그 문제는 'data_start'에서 발생했다고 한다
- 다른 분석 도구를 사용해서 보니까 적어넣은 'WinMain'과 문제 있는 'data_start'의 주소가 같다?
- 또 다른 분석 도구를 사용해서 보니까 'WinMainCRTStartup'이 'WinMain'으로 점프하게 되어있다?
- 그래서 일단
crt2.o를 멱살 잡고 보니까 여기서 'WinMainCRTStartup'을 부르는 것 같애 - 'tmainCRTStartup'을 불러서 'main'으로 진입하고 있음
- main은 'WinMainCRTStartup'을 부르는
crt2.o가libmingw32.a의lib64_libmingw32_a-crtexewin.o에서 가져왔음 lib64_libmingw32_a-crtexewin.o은 main에서 'WinMain' 으로 진입함- 'WinMain'을 '실행' 하러 갔더니 실행이 불가능한
.data섹션이어서 오류가 발생함

왜 실행이 불가능했을까요?
- 변수 할당은
.data나.rdata섹션으로 들어가고, 실행될 수 없습니다.int WinMain = 1;은 실행 가능한 코드가 아니고 이 값이 저 섹션으로 들어가고 맙니다. - 어찌되었든 그 섹션으로 향하면 실행이 불가능하므로 SIGSEGV를 뱉으며 Segmentation fault가 발생.
그러면... 'WinMain'을 함수로 만들면 되는 거 아냐...?
- 'WinMain'을 함수로 작성하고,
gcc hello.c를 실행시키기

int WinMain(){};ㄱ-
함수의 문법에 따라 이렇게 작성하면... 문제가 없을까요?
참고로 함수의 문법은
반환타입 함수이름(매개변수1, 매개변수2, ...) {
// 함수가 실행할 코드
return 값; // 반환값이 있는 경우
}대략적으로 이렇습니다. 최소화해서 저렇게 넣으면 문제가 없을까요? 한 번 gcc를 갈궈봅시다.


그러면... 이번에는 GDB로 추적해보면 어떤 결과를 볼 수 있을까요?
GNU gdb (GDB) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.exe...
(gdb) run
Starting program: I:\a.exe
[New Thread 27716.0x12c]
[Thread 27716.0x12c exited with code 10]
[Inferior 1 (process 27716) exited with code 012]문제가... 없다...?
그렇습니다... 우리가 맞춘 퍼즐에서는 이것은 전~~혀 문제가 없습니다.
이 이후는 다음 글에 이어서...