[Linux] 리눅스 개발환경 도구 ①

예빈·2025년 12월 18일

Embedded/Linux

목록 보기
10/21


0️⃣ 들어가며

리눅스의 기초라고 할 수 있는 리눅스 이해하기 편이 끝났다.
이번 포스팅에서는 리눅스 개발환경에서 유용하게 쓸 수 있는 툴체인을 정리해 볼 예정이다.
내용이 아주 딥하지는 않고 개요와 가벼운 설명 위주로 정리되어 있다.


1️⃣ 학습 내용

Toolchain의 개요

✅ 개발 관련 도구

  • 컴파일/빌드 도구

    gcc - C/C++ 컴파일러

    make - 빌드 자동화(Makefile)

    cmake - 프로스플랫폼 빌드(CMakeLists.txt)

  • 디버깅 도구

    gdb - GNU Debugger

  • 라이브러리

    정적 라이브러리

    공유 라이브러리

    동적 라이브러리

  • 버전관리/배포 도구

    git - 로컬 버전관리 / GitHub - 원격 저장소

    docker - 컨테이너화 배포

✅ Toolchain

  • 리눅스의 Toolchain

    소스코드로부터 실행파일을 만드는 데 필요한 전체 도구의 집합

    Unix/Linux는 C로 만들어졌기 때문에 gcc가 기본 포함되어 있음

  • cc(표준 C 컴파일러)

    리눅스에서 내부적으로 gcc 심볼릭 링크되어 있음

  • gcc(GNU Compiler Collection)

    과거 GNU C Compiler였으나 다른 언어와 확장자를 지원하면서 이름 변경

    파일 확장자 자동 인식 후 적절한 컴파일러 호출

    확장자호출 도구
    .ccc1(C 컴파일러)
    .cpp/.Cg++(C++ 컴파일러)
    .sgas(어셈블러)
  • Toolchain의 구성 요소

    도구역할명령어 예시
    cppC Preprocessor(#include, #define 처리)gcc -E file.c
    cc1C Compiler(.c → .s 어셈블리)gcc 내부 호출
    g++C++ Compilerg++ file.cpp
    gasGNU Assembler(.s → .o 오브젝트)gcc -c file.s
    ld(collect2)Linker(.o + 라이브러리 → 실행파일)gcc -o app *.o
    gdbDebuggergdb ./app
    objcopy오브젝트 변환(.o → .bin/.hex 임베디드용)objcopy -O binary

✅ Native Compiler / Cross Compiler

  • Native Compiler / Cross Compiler

    호스트와 타겟이 같은 경우 Native Compiler를 쓰고, 다른 경우 Cross Compiler 사용

    구분Native CompilerCross Compiler
    호스트x86_64 (PC)x86_64 (PC)
    타겟x86_64 (PC 실행)aarch64 (Raspberry Pi 등)
    예시gcc hello.carm-linux-gnu-gcc hello.c
    file 결과ELF x86-64ELF 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 기록런타임 동작
      .TEXTROM포함ROM에 기록실행
      .RODATAROM포함ROM에 기록읽기
      .DATARAM초기값만 포함ROM에 기록RAM으로 복사
      .BSSRAM제외 (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

Debug

✅ GDB

  • 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 명령어

  • 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 레지스터의 값 확인

✅ GDB 사용 예시

  • 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) 라이브러리

    빌드할 때 라이브러리 코드가 실행 파일 안에 복사되는 형태

    실행 파일 하나만 배포해도 되고 버전 변동의 영향이 적지만 용량이 큼

  • 정적 라이브러리의 사용

    1. 개별 소스를 오브젝트 파일로 만들기

      $ gcc -c mylib.c      # mylib.o 생성

      -c 옵션으로 오브젝트 파일만 생성

    2. 아카이브 만들기

      $ ar r libmylib.a mylib.o # libmylib.a 안에 mylib.o가 들어감
      ar: creating libmylib.a
      $ ar t libmylib.a
      mylib.o

      ar : 아카이브 툴

      r : 기존 아카이브에 교체/추가

      t : 아카이브 내용 확인

    3. 정적 라이브러리 링크

      $ 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 추가

    -lmyliblibmylib.so 또는 libmylib.a를 찾아서 사용

    여기까지 하면 실행 파일 생성은 성공하지만, 실행 시 런타임 로더가 so 위치를 못 찾으면 오류 발생

  • 공유 라이브러리의 로딩

    • 실행 시에 ldd(로더)가 찾는 라이브러리 경로

      1. LD_LIBRARY_PATH 환경 변수
      2. /etc/ld.so.conf와 ldconfig로 등록된 시스템 경로
      3. 기본 디렉토리 (/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

2️⃣ 느낀 점

분명 저번 주에 리눅스 이해 편을 잔뜩 썼는데, 잠깐 정리 못 하는 사이에 내용이 엄청 많이 밀렸다.
이거 다 쓸 수는 있는 건가 걱정이 되기 시작했다.
다음 편에서는 make와 Makefile, CMake를 다룰 예정이라 아주 유용할 것이다!

0개의 댓글