
리눅스의 기초라고 할 수 있는 리눅스 이해하기 편이 끝났다.
이번 포스팅에서는 리눅스 개발환경에서 유용하게 쓸 수 있는 툴체인을 정리해 볼 예정이다.
내용이 아주 딥하지는 않고 개요와 가벼운 설명 위주로 정리되어 있다.
컴파일/빌드 도구
gcc - C/C++ 컴파일러
make - 빌드 자동화(Makefile)
cmake - 프로스플랫폼 빌드(CMakeLists.txt)
디버깅 도구
gdb - GNU Debugger
라이브러리
정적 라이브러리
공유 라이브러리
동적 라이브러리
버전관리/배포 도구
git - 로컬 버전관리 / GitHub - 원격 저장소
docker - 컨테이너화 배포
리눅스의 Toolchain
소스코드로부터 실행파일을 만드는 데 필요한 전체 도구의 집합
Unix/Linux는 C로 만들어졌기 때문에 gcc가 기본 포함되어 있음
cc(표준 C 컴파일러)
리눅스에서 내부적으로 gcc 심볼릭 링크되어 있음
gcc(GNU Compiler Collection)
과거 GNU C Compiler였으나 다른 언어와 확장자를 지원하면서 이름 변경
파일 확장자 자동 인식 후 적절한 컴파일러 호출
| 확장자 | 호출 도구 |
|---|---|
| .c | cc1(C 컴파일러) |
| .cpp/.C | g++(C++ 컴파일러) |
| .s | gas(어셈블러) |
Toolchain의 구성 요소
| 도구 | 역할 | 명령어 예시 |
|---|---|---|
| cpp | C Preprocessor(#include, #define 처리) | gcc -E file.c |
| cc1 | C Compiler(.c → .s 어셈블리) | gcc 내부 호출 |
| g++ | C++ Compiler | g++ file.cpp |
| gas | GNU Assembler(.s → .o 오브젝트) | gcc -c file.s |
| ld(collect2) | Linker(.o + 라이브러리 → 실행파일) | gcc -o app *.o |
| gdb | Debugger | gdb ./app |
| objcopy | 오브젝트 변환(.o → .bin/.hex 임베디드용) | objcopy -O binary |
Native Compiler / Cross Compiler
호스트와 타겟이 같은 경우 Native Compiler를 쓰고, 다른 경우 Cross Compiler 사용
| 구분 | Native Compiler | Cross Compiler |
|---|---|---|
| 호스트 | x86_64 (PC) | x86_64 (PC) |
| 타겟 | x86_64 (PC 실행) | aarch64 (Raspberry Pi 등) |
| 예시 | gcc hello.c | arm-linux-gnu-gcc hello.c |
| file 결과 | ELF x86-64 | ELF aarch64 |
전체 흐름
hello.c ─(1)Preprocess─> hello.i ─(2)Compile─> hello.s
cpp gcc -E gcc -S
─(3)Assemble─> hello.o ─(4)Link─> hello (ELF 실행파일)
gcc -c gcc -o
Preprocess(전처리)
#include , #define 등을 처리하는 과정
gcc -E hello.c -o hello.i
Compile
C언어로 된 파일을 어셈블리 소스(.s , ASCII)로 만드는 과정
gcc -S hello.i -o hello.s
file hello.s
# hello.s: assembler source, ASCII text
Assemble
ASM 소스(.s)를 Object 파일(.o, ELF)로 만드는 과정
gcc -c hello.s -o hello.o
file hello.o
# hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
objdump -h hello.o
# hello.o: file format elf64-x86-64
# Sections:
# Idx Name Size VMA LMA File off Algn
# 0 .text 0000001b 0000000000000000 0000000000000000 00000040 2**0
# CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
# 1 .data 00000000 0000000000000000 0000000000000000 0000005b 2**0
# CONTENTS, ALLOC, LOAD, DATA
# ...
Locate
Relocatable Object 파일을 Located로 변환하는 과정으로, Absolute Address를 지정함
임베디드에서는 ROM/RAM의 주소가 보드마다 다르기 때문에 링커 스크립트(.ld)로 직접 지정해 줘야 함
func() → &func = 0x08000100 /* .text (ROM 고정) */
int gi=10 → &gi = 0x20000100 /* .data (RAM, ROM에 초기값) */
int li; → &li = 0x20001000+sp /* 스택 (런타임 동적) */
Link
여러 개의 Object 파일(.o)과 라이브러리 파일(.a, .so)을 더해서 최종적으로 단일 실행파일(ELF)을 생성하는 과정
심볼 해결, 주소 할당, 런타임 초기화 코드 삽입을 포함
gcc -o hello hello.o
objdump -h hello
# hello: file format elf64-x86-64
# Sections:
# Idx Name Size VMA LMA File off Algn
# 0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0
# CONTENTS, ALLOC, LOAD, READONLY, DATA
# 1 .note.gnu.property 00000020 0000000000000338 0000000000000338 00000338 2**3
# CONTENTS, ALLOC, LOAD, READONLY, DATA
# ...
# 24 .data 00000010 0000000000004000 0000000000004000 00003000 2**3
# CONTENTS, ALLOC, LOAD, DATA
# 25 .bss 00000008 0000000000004010 0000000000004010 00003010 2**0
# ALLOC
# 26 .comment 0000002b 0000000000000000 0000000000000000 00003010 2**0
# CONTENTS, READONLY
elf2bin/elf2hex
ELF 파일의 내용을 ROMable file(.bin/.hex)로 변환하여 Flash 메모리가 인식할 수 있는 순수 바이너리 형태로 만드는 과정
실행 섹션만 추출하고 주소와 메타데이터 정보는 사라짐
firmware.elf (실행가능, 45KB)
┌──────────────┐
│ ELF Header │ 메타데이터 (64B)
├──────────────┤
│ .text │ 코드 (ROM: 0x08000100)
│ .rodata │ 상수 (ROM)
│ .data │ 초기값 (RAM, ROM에 저장)
│ Symbol Table │ 디버깅 정보 (수 KB)
│ .bss │ 0초기화 영역 (RAM, 런타임)
└──────────────┘
firmware.bin (ROMable, 4KB)
┌──────────────┐
│ .text │
│ .rodata │ ← **실행 섹션만 추출**
│ .data 초기값 │
└──────────────┘ ← 주소/메타 제거
| 섹션 | LMA(VMA) | elf2bin 결과 | ROM 기록 | 런타임 동작 |
|---|---|---|---|---|
.TEXT | ROM | 포함 | ROM에 기록 | 실행 |
.RODATA | ROM | 포함 | ROM에 기록 | 읽기 |
.DATA | RAM | 초기값만 포함 | ROM에 기록 | RAM으로 복사 |
.BSS | RAM | 제외 (0) | 기록 안함 | 런타임 0으로 채움 |
빌드 프로세스와 에러 예시
# main()이 있는 print.c와 print()가 있는 hello.c
# 단일 파일 컴파일 시도
$ cc print.c
print.c: In function ‘main’:
print.c:5:2: warning: implicit declaration of function ‘print’; did you mean ‘printf’? [-Wimplicit-function-declaration]
5 | print("hello world\n");
| ^~~~~
| printf
/usr/bin/ld: /tmp/ccQDuPlm.o: in function `main':
print.c:(.text+0x15): undefined reference to `print'
collect2: error: ld returned 1 exit status
# gcc print.c 단일 파일 컴파일 시도
# ↓
# Preprocessing → print.i #include <stdio.h> 확장
# Compilation → print.s print() 정의 (심볼 생성)
# Assembly → print.o OK (정의만 있음)
# Linking → undefined reference to `print'
# ↑ 링크 단계 실패!
1. print.h 생성
└── extern void print(const char*); # 선언만!
2. hello.c 수정
#include "print.h" # 선언 임포트
3. 분리 컴파일
gcc -c print.c → print.o (정의)
gcc -c hello.c → hello.o (선언+호출)
gcc hello.o print.o → helloworld (성공!)
$ gcc -c hello.c
hello.c: In function ‘main’:
hello.c:5:2: warning: implicit declaration of function ‘print’; did you mean ‘printf’? [-Wimplicit-function-declaration]
5 | print("hello world\n");
| ^~~~~
| printf
$ gcc -c print.c
$ gcc -o hello hello.o print.o
$ ./hello
hello world
GDB의 정의
GNU Debugger
GNU 소프트웨어 컬렉션의 핵심 디버깅 도구로, Unix/Linux/macOS에서 표준 디버거로 활용
GDB 디버깅 구성
Host PC (Eclipse IDE)
│ TCP/IP
└──────────► Server PC (Ubuntu)
│ USB (CDC)
└──────────► Debug Probe
│ SWD/JTAG
└──────────► Target Board
디버깅 옵션 설정
gcc를 사용할 때 -g 옵션을 주어야 운영 체제의 디버깅 형식(DWARF-2)으로 디버깅 정보를 생성
-g 없이 GDB를 사용하면 심볼 테이블이 없다는 에러 발생
$ gcc -g -O0 -o program source.c
# -g : 심볼 테이블, 라인넘버, 소스 경로 삽입
# -w : warning을 사용하지 않음
# -O0 : 최적화 끄기 (디버깅 정확도 향)
GDB 시작과 도움말
gcc -g demo.c -o demo # 디버그 빌드 필수
gdb ./demo # 실행파일 로드
(gdb) help # 대분류: Status, Running, Breakpoints...
(gdb) help list # 소스 보기 도움말
(gdb) help status # 상태 관련 명령
소스코드 탐색
| 명령 | 기능 | 예시 |
|---|---|---|
list / l | 소스코드 표시 | l (현재 위치 10라인) |
list main | 함수 전체 | l main |
frame / f | 현재 프레임 라인 | f (중단점 위치) |
list 1,20 | 라인 범위 | l 1,20 |
(gdb) list
1 #include <stdio.h>
2 int main() {
3 int sum = 0;
4 for(int i=0; i<10; i++) {
5 sum += i; ← 현재 위치
6 }
(gdb) frame
#0 main () at demo.c:5
실행 제어와 브레이크포인트
실행 시작
(gdb) run / r # 인자: r arg1 arg2
(gdb) run # main()부터 실행 → 첫 브레이크까지
브레이크포인트 설정
(gdb) b main # 함수 시작
(gdb) b 15 # 라인 번호
(gdb) b func if x>10 # 조건부
상태 확인/제어
(gdb) info breakpoints / i b # 중단점 목록 (Num=1, Disp=keep, Enabled=y...)
(gdb) disable 1 # 중단 (Num=1)
(gdb) enable 1 # 재시작
(gdb) delete 1 # 삭제
싱글 스텝 실행
| 명령 | 기능 | 특징 |
|---|---|---|
step / s | 함수 내부로 진입 | printf() 안으로 들어감 |
next / n | 함수 건너뛰기 | printf() 한 라인으로 처리 |
finish / fin | 현재 함수 종료까지 | 리턴값 확인 |
continue / c | 다음 브레이크까지 | c |
(gdb) b main
(gdb) r
Breakpoint 1, main () at demo.c:5
(gdb) n # for문 다음 라인
(gdb) s # sum += i; 내부 싱글스텝
(gdb) c # 끝날때까지 계속
변수 검사
변수 출력 (print)
(gdb) print sum / p sum # 현재 값: $1 = 45
(gdb) print arr[3] # 배열 요소
(gdb) print *ptr # 포인터 역참조
(gdb) print/x 0x123456 # 16진수 출력
자동 갱신 (display)
(gdb) display sum # 매 스텝마다 자동 출력
1: sum = 45
(gdb) undisplay 1 # 해제
변경 감지, 워치포인트 (watch)
(gdb) watch sum # sum 변경시 자동 중단
Hardware watchpoint 2: sum
스택 & 심볼
스택 분석
(gdb) backtrace / bt # 호출 스택 전체
(gdb) bt full # 로컬 변수 포함
(gdb) frame 2 # 2번째 프레임 이동
(gdb) up / down # 스택 위/아래
심볼 업데이트
(gdb) file demo_new # 새 실행파일 로드 (소스 변경시)
Reading symbols from demo_new...
(gdb) symbol-file demo_new # 심볼만 업데이트
레지스터 & 문맥 (Context)
레지스터 전체
(gdb) info registers / i reg
rax 0x7fffffffde40 140737488346176
rbx 0x0 0
rip 0x401159 0x401159 <main+25>
특정 레지스터
(gdb) i reg eax # eax 레지스터의 값 확인
Segmentation fault 코드
#include <stdio.h>
int main() {
puts("hello"); // 실행
*(volatile unsigned int*)(main+1) = 1; // SIGSEGV 시그널
// main 함수가 .text 영역(읽기전용)에 쓰기 시도
// segmentation fault는 주로 포인터를 잘못 사용했을 때 발생
puts("world"); // 도달 불가
return 0;
}
gcc -g demo_SIGSEGV.c -o demo # -g: 디버그 심볼 포함
ulimit -c unlimited # 코어 덤프 크기 무제한
./demo # Segmentation fault (core dumped)
gdb ./demo core.12345 # 주의 : 가상환경에서는 코어 덤프 확인 불가
(gdb) list # 소스코드 + 에러 라인 표시
11 *(volatile unsigned int*)(main+1) = 1;
(gdb) info frame # 프레임 정보 (스택, 인자)
(gdb) quit
GDB로 코어 덤프 분석
(gdb) bt # 백트레이스 (호출 스택)
#0 0x0000555555555159 in main () at demo_SIGSEGV.c:11
(gdb) info registers # 레지스터 상태
(gdb) x/10i $pc-10 # PC 주변 어셈블리
(gdb) print *(int*)0x555555555159 # 잘못된 메모리 내용
(gdb) disassemble main # main 함수 디스어셈블리
라이브러리의 종류
| 종류 | 링크 시점 | 파일 형태 |
|---|---|---|
| 정적 라이브러리 | 컴파일/링크 시 | .a (리눅스) |
| 공유/동적 라이브러리 | 실행 시 (런타임) | .so (리눅스) |
정적(Static) 라이브러리
빌드할 때 라이브러리 코드가 실행 파일 안에 복사되는 형태
실행 파일 하나만 배포해도 되고 버전 변동의 영향이 적지만 용량이 큼
정적 라이브러리의 사용
개별 소스를 오브젝트 파일로 만들기
$ gcc -c mylib.c # mylib.o 생성
-c 옵션으로 오브젝트 파일만 생성
아카이브 만들기
$ ar r libmylib.a mylib.o # libmylib.a 안에 mylib.o가 들어감
ar: creating libmylib.a
$ ar t libmylib.a
mylib.o
ar : 아카이브 툴
r : 기존 아카이브에 교체/추가
t : 아카이브 내용 확인
정적 라이브러리 링크
$ cc -o calc main.c -Llib -lmylib
컴파일할 때 사용할 라이브러리를 링크해 주어야 함
-o <output> : 출력 실행 파일 이름을 output으로 설정
-L<searchdir> : 라이브러리 검색 경로에 ./searchdir 디렉토리 추가
-l<namespec> : lib<namespec>.a 또는 lib<namespec>.so 를 찾도록 함
공유 라이브러리
여러 프로그램이 같은 라이브러리 파일 하나를 사용하지만 각 프로세스에서 로딩 주소는 다를 수 있음
실제 물리 메모리에서는 코드 페이지를 공유하기 때문에 중복으로 올라가지는 않음
(예: calc에서 libmylib.so 기준 주소는 0x00007f...a000 ,cald에서는 0x00007f...e000)
공유 라이브러리 만들기
# 1) PIC 코드로 컴파일 (위치 독립 코드)
cc -fPIC -c mylib.c # mylib.o
# 2) 공유 라이브러리 생성
cc -shared -Wl,-soname=libmylib.so -o libmylib.so.1 mylib.o
# 3) 버전 없는 이름으로 심볼릭 링크
ln -s libmylib.so.1 libmylib.so
-fPIC : 어느 주소에 로딩되어도 동작 가능한 Position Independent Code 생성
-shared : 공유 라이브러리 생성 모드
-Wl,-soname=... : 링커에게 실제 so 이름(SONAME)을 알려줌
공유 라이브러리 링크
$ cc -c main.c # main.o
$ cc -o addsub main.o -Llib -lmylib
$ ./addsub
# error while loading shared libraries: libmylib.so: cannot open shared object file
-Llib : 라이브러리 검색 경로에 ./lib 추가
-lmylib : libmylib.so 또는 libmylib.a를 찾아서 사용
여기까지 하면 실행 파일 생성은 성공하지만, 실행 시 런타임 로더가 so 위치를 못 찾으면 오류 발생
공유 라이브러리의 로딩
실행 시에 ldd(로더)가 찾는 라이브러리 경로
LD_LIBRARY_PATH 환경 변수/etc/ld.so.conf와 ldconfig로 등록된 시스템 경로/lib, /usr/lib 등)LD_LIBRARY_PATH 에 라이브러리 경로 추가
# 환경변수에 경로 지정 후 실행하면 정상 작동
$ export LD_LIBRARY_PATH=lib
동적 라이브러리
실행 중에 동적으로 라이브러리를 로드/언로드하는 방식
만드는 방법은 공유 라이브러리와 동일함
빌드할 때는 libdl 을 함께 링크해 주어야 함
라이브러리 사용방법 비교
공유 라이브러리 (동적 링크)
프로그램을 시작한 뒤에 로더가 .so 를 자동으로 로드해서 메모리에 올리는 방식
동적 라이브러리 (런타임 동적 로드, dlopen )
프로그램이 실행 중에 라이브러리가 필요할 때만 .so 를 열고 쓴 뒤에 닫는 방식
주요 함수
#include <dlfcn.h> 내에 정의
dlopen : .so 파일 오픈
dlsym : 함수 심볼 가져오기
dlerror : 에러 메시지 확인
dlclose : 라이브러리 닫기
예시
#include <dlfcn.h>
// 1. 라이브러리 열기
void *handle = dlopen("libmylib.so", RTLD_NOW);
if (!handle) {
fprintf(stderr, "dlopen error: %s\n", dlerror());
return 1;
}
// 2. 심볼(함수) 찾기
int (*add)(int,int);
add = (int (*)(int,int)) dlsym(handle, "add");
// 3. 사용
int r = add(3, 5);
printf("add: %d\n", r);
// 4. 닫기
dlclose(handle);
// build
// $ cc -o calc main.c -ldl
분명 저번 주에 리눅스 이해 편을 잔뜩 썼는데, 잠깐 정리 못 하는 사이에 내용이 엄청 많이 밀렸다.
이거 다 쓸 수는 있는 건가 걱정이 되기 시작했다.
다음 편에서는 make와 Makefile, CMake를 다룰 예정이라 아주 유용할 것이다!