BusyBox 종합 가이드
임베디드 리눅스의 핵심 유틸리티 모음인 BusyBox를 종합적으로 다룹니다. 멀티콜 바이너리(Multicall Binary) 아키텍처, 애플릿(Applet) 디스패치 메커니즘, 심볼릭 링크(Symbolic Link) 시스템, menuconfig 설정, init 시스템, initramfs 활용, 크기 최적화까지 BusyBox의 모든 것을 설명합니다.
ls, cp, sh, mount, init 등 수백 개의 유닉스 명령어가
하나의 실행 파일에 들어있습니다. 크기는 1MB 미만이면서 전체 리눅스 시스템을 구동할 수 있습니다.
핵심 요약
- 멀티콜 바이너리 — 하나의 실행 파일(
/bin/busybox)에 수백 개의 유닉스 유틸리티가 포함되어 있으며,argv[0](호출된 이름)으로 어떤 명령어를 실행할지 결정합니다. - 심볼릭 링크 —
/bin/ls,/bin/sh등이 모두/bin/busybox로의 심볼릭 링크입니다. 링크 이름이 곧 실행할 애플릿을 결정합니다. - 애플릿(Applet) — BusyBox에 포함된 각 유틸리티를 애플릿이라 합니다. menuconfig로 필요한 애플릿만 선택하여 빌드 크기를 조절합니다.
- init 시스템 — BusyBox는 자체 init을 제공하여
/etc/inittab기반으로 임베디드 시스템을 부팅합니다. - 크기 — 기본 설정(defconfig) 약 1MB, 최소 설정 약 200KB. GNU coreutils(15MB+) 대비 1/10 이하입니다.
핵심 용어 정리
| 용어 | 설명 |
|---|---|
| 멀티콜 바이너리(Multicall Binary) | 하나의 ELF 실행 파일이 호출 이름에 따라 다른 기능을 수행하는 구조 |
| 애플릿(Applet) | BusyBox에 포함된 개별 유틸리티. 각 애플릿은 xxx_main() 함수로 구현됨 |
| argv[0] 디스패치 | 프로그램이 실행될 때 argv[0](프로그램 이름)을 검사하여 해당 애플릿을 호출하는 메커니즘 |
| 심볼릭 링크(Symlink) | /bin/ls → /bin/busybox 처럼 각 명령어 이름이 busybox 바이너리를 가리키는 링크 |
| menuconfig | ncurses 기반 설정 인터페이스. 애플릿 선택, 정적/동적 링크, 기능 플래그 등을 설정 |
| ash | BusyBox의 기본 셸. Almquist Shell 기반으로 POSIX 호환이며 bash보다 가볍지만 일부 bash 확장을 지원하지 않음 |
| mdev | BusyBox의 경량 디바이스 관리자. udev 대안으로 임베디드 환경에서 /dev 노드를 동적 생성 |
| inittab | BusyBox init이 읽는 설정 파일. 부팅 시 실행할 프로세스와 런레벨 동작을 정의 |
단계별 이해
BusyBox를 처음 접하는 개발자를 위한 5단계 가이드입니다:
1단계: BusyBox 다운로드 및 빌드
# BusyBox 소스 다운로드
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar xjf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
# 기본 설정으로 빌드
make defconfig
make -j$(nproc)
# 설치 (기본: _install 디렉토리)
make install
2단계: 설치 결과 확인
# 심볼릭 링크 확인
ls -la _install/bin/
# lrwxrwxrwx 1 user user 7 ... ash -> busybox
# lrwxrwxrwx 1 user user 7 ... cat -> busybox
# lrwxrwxrwx 1 user user 7 ... ls -> busybox
# -rwxr-xr-x 1 user user 1.1M ... busybox
# BusyBox에 포함된 애플릿 목록 확인
./busybox --list | head -20
3단계: menuconfig로 커스터마이징
# ncurses 기반 설정 인터페이스
make menuconfig
# 주요 설정 항목:
# Settings → Build static binary (no shared libs) ← 정적 링크
# Settings → Cross compiler prefix ← 크로스 컴파일
# Shells → ash ← 셸 선택
# Init Utilities → init ← init 시스템
4단계: initramfs에서 BusyBox 사용
# 최소 initramfs 디렉토리 구성
mkdir -p initramfs/{bin,sbin,etc,proc,sys,dev,tmp,usr/{bin,sbin}}
# BusyBox 정적 빌드 복사
cp busybox initramfs/bin/
cd initramfs/bin && ./busybox --install -s .
cd ../sbin && ../bin/busybox --install -s .
# init 스크립트 작성
cat > initramfs/init <<'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
echo "BusyBox initramfs booted!"
exec /bin/sh
EOF
chmod +x initramfs/init
5단계: QEMU로 테스트
# cpio 아카이브 생성
cd initramfs
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
cd ..
# QEMU 부팅 (x86_64)
qemu-system-x86_64 -kernel bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0" -nographic
개요 및 역사
BusyBox란
BusyBox는 하나의 실행 파일에 수백 개의 유닉스(Unix) 유틸리티를 통합한 소프트웨어입니다.
"임베디드 리눅스의 스위스 아미 나이프(The Swiss Army Knife of Embedded Linux)"라는 공식 슬로건처럼,
ls, cp, sh, mount, init, ifconfig, wget 등
일반적인 리눅스 시스템에 필요한 거의 모든 기본 명령어를 단일 바이너리로 제공합니다.
멀티콜 바이너리 구조 덕분에 GNU coreutils, util-linux, bash 등을 개별 설치하는 것보다 디스크/플래시(Flash) 공간과 메모리를 극적으로 절약합니다. 전체 루트 파일시스템을 1MB 미만으로 구성할 수 있어 임베디드 시스템, initramfs, 컨테이너(Container) 베이스 이미지, 복구(Recovery) 환경에 광범위하게 사용됩니다.
주요 사용 환경:
| 환경 | BusyBox 역할 | 대표 사례 |
|---|---|---|
| 임베디드 시스템 | 전체 유저 공간 유틸리티 제공. 4~16MB 플래시에 리눅스 시스템 구동 | 라우터(OpenWrt), IP 카메라, 산업용 게이트웨이, IoT 장비 |
| initramfs | 커널 부팅 초기 루트 파일시스템. 하드웨어 초기화, 루트 마운트, switch_root | 리눅스 배포판 설치 디스크, 커널 개발/디버깅 환경 |
| 컨테이너 | 최소 베이스 이미지(~1.2MB). 셸과 기본 유틸리티 제공 | Alpine Linux (Docker 사실상 표준), 사이드카 컨테이너 |
| 복구 환경 | 시스템 복구 시 최소 유틸리티 제공. 디스크 마운트, 파일 복사, 네트워크 설정 | Android Recovery, 데비안 설치 디스크, 시스템 복구 USB |
| 네트워크 부팅 | PXE/TFTP 부팅 시 initramfs 기반 경량 OS 제공 | 디스크리스(Diskless) 서버, 네트워크 진단 도구 |
역사와 발전
| 연도 | 이벤트 |
|---|---|
| 1996 | Bruce Perens가 데비안(Debian) 설치 디스크를 위해 BusyBox 최초 개발. 플로피 디스크 1장(1.44MB)에 들어가는 리눅스 시스템 목표 |
| 1999 | Erik Andersen이 메인테이너를 이어받아 대규모 리팩토링. uClibc 프로젝트와 함께 임베디드 리눅스 생태계 확립 |
| 2006 | Denys Vlasenko가 메인테이너 역할 시작. 현재까지 활발히 유지보수 |
| 2007~2011 | Software Freedom Conservancy가 GPL 준수를 위한 소송 다수 진행 (Verizon, Samsung, Westinghouse 등) |
| 2015+ | Docker Alpine 이미지 기반으로 컨테이너 환경에서 BusyBox 활용 급증 |
| 현재 | 1.36.x 안정 릴리스. 약 400개 애플릿 지원. OpenWrt, Buildroot, Android Recovery 등에서 기본 채택 |
주요 버전별 변경사항:
| 버전 | 릴리스 | 주요 변경 |
|---|---|---|
| 1.00 | 2004-10 | 첫 안정 릴리스. Erik Andersen의 대규모 리팩토링 완료 |
| 1.10 | 2008-01 | ash 셸 대폭 개선, IPv6 지원, SELinux 지원 추가 |
| 1.20 | 2012-08 | Unicode(유니코드) 지원, ntpd 애플릿 추가, 빌드 시스템 개선 |
| 1.25 | 2016-08 | ssl_client 추가 (HTTPS wget 지원), tc 네트워크 트래픽 제어 |
| 1.30 | 2019-01 | bc 임의 정밀도 계산기, 64비트 time_t, CONFIG_FEATURE_PREFER_APPLETS 개선 |
| 1.35 | 2022-01 | xxd 애플릿 추가, ip 명령 개선, 빌드 경고 수정 |
| 1.36 | 2023-01 | ash HISTFILE 지원, tsort 추가, tree 추가, musl 빌드 개선 |
| 1.37 | 2024-11 | 최신 안정 릴리스. seedrng, nproc 추가, 다수 버그 수정 |
라이선스
BusyBox는 GPLv2 라이선스입니다. GPL 소송 역사에서 중요한 위치를 차지하는데, Software Freedom Conservancy(SFC)가 BusyBox를 포함한 제품의 GPL 위반을 다수 소송하여 임베디드 업계의 오픈소스(Open Source) 컴플라이언스(Compliance) 인식을 크게 높였습니다.
대안 프로젝트 비교
| 프로젝트 | 라이선스 | 주요 목적 | 애플릿 수 | 특징 |
|---|---|---|---|---|
| BusyBox | GPLv2 | 범용 임베디드 유틸리티 | ~400 | 가장 성숙하고 검증된 프로젝트. OpenWrt/Buildroot 기본 채택 |
| toybox | 0BSD | BSD 라이선스 대안 | ~230 | Android에서 BusyBox 대체. Rob Landley(전 BusyBox 메인테이너) 주도 |
| klibc | BSD/GPL | initramfs 전용 | ~30 | 커널 부팅 초기에 필요한 최소 유틸리티만 제공 |
| dash | BSD | POSIX 셸 전용 | 1 (sh) | 데비안/우분투 기본 /bin/sh. 셸만 필요한 경우 최적 |
멀티콜 바이너리 아키텍처
멀티콜 바이너리 개념
일반적인 리눅스 시스템에서 ls, cp, cat 등은 각각 독립 실행 파일입니다.
BusyBox는 이 모든 명령어를 하나의 ELF 바이너리에 통합합니다.
실행 시 argv[0](프로세스 이름)을 검사하여 어떤 애플릿을 실행할지 결정합니다:
/bin/ls가/bin/busybox로의 심볼릭 링크일 때, 커널이 실제로/bin/busybox를 실행- BusyBox의
main()은argv[0]에서"ls"를 추출 - 정렬된 애플릿 테이블에서 이진 탐색(Binary Search)으로
"ls"를 찾음 - 매칭된
ls_main()함수를 호출하여ls기능 수행
이 구조의 핵심 이점은 코드 공유입니다. 문자열 처리, 파일 I/O, 에러 핸들링(Error Handling) 등의 공통 함수를 모든 애플릿이 공유하므로, 개별 바이너리를 합산한 크기보다 훨씬 작습니다.
/* BusyBox 멀티콜 디스패치 핵심 흐름 (단순화) */
int main(int argc, char **argv)
{
/* argv[0]에서 경로 제거: "/bin/ls" → "ls" */
const char *applet_name = bb_basename(argv[0]);
/* 정렬된 애플릿 테이블에서 이진 탐색 */
int idx = find_applet_by_name(applet_name);
if (idx >= 0)
return applet_main[idx](argc, argv); /* ls_main() 호출 */
/* "busybox" 자체로 실행된 경우 */
if (strcmp(applet_name, "busybox") == 0)
return busybox_main(argc, argv);
error_msg_and_die("applet not found: %s", applet_name);
}
BusyBox의 크기 효율성은 libbb(BusyBox 내부 라이브러리)의 코드 공유에서 비롯됩니다.
모든 애플릿이 공유하는 핵심 함수들은 libbb/ 디렉토리에 있으며,
개별 바이너리에서는 각각 중복 구현해야 할 기능을 한 번만 구현합니다:
| libbb 모듈 | 제공 함수 | 절약 효과 |
|---|---|---|
xfuncs.c | xmalloc(), xopen(), xread() — 에러 시 자동 종료하는 래퍼 | 모든 애플릿에서 에러 처리 코드 제거 |
messages.c | 공통 에러/경고 메시지 문자열 상수 | 중복 문자열 리터럴 제거 (~2KB 절약) |
getopt32.c | getopt32() — 옵션 파싱 통합 API | getopt_long() 대비 훨씬 작은 옵션 파서 |
copyfd.c | bb_copyfd_eof(), bb_copyfd_size() | 파일 복사 로직 공유 (cp, cat, dd 등) |
recursive_action.c | recursive_action() — 재귀 디렉토리 순회 | find, rm -r, chmod -R 등이 공유 |
read.c | xmalloc_fgets(), bb_get_chunk_from_file() | 라인 단위 읽기 통합 |
perror_msg.c | bb_perror_msg(), bb_error_msg_and_die() | 에러 출력 형식 통일 |
appletlib.c | 애플릿 디스패치, SUID 처리, 시그널 초기화 | 멀티콜 바이너리 프레임워크 핵심 |
/* libbb/xfuncs.c — 에러 처리 래퍼 예시 */
void* FAST_FUNC xmalloc(size_t size)
{
void *ptr = malloc(size);
if (ptr == NULL && size != 0)
bb_die_memory_exhausted();
return ptr;
}
/* 모든 애플릿에서 malloc 대신 xmalloc 사용:
* - NULL 체크 코드가 각 애플릿에서 제거됨
* - 일관된 에러 메시지 출력
* - 바이너리 크기 절감 */
이 설계 덕분에 개별 애플릿의 xxx_main() 함수는 핵심 로직에만 집중하며,
보일러플레이트(Boilerplate) 코드 없이 매우 짧게 구현됩니다.
예를 들어 BusyBox의 yes 애플릿은 10줄 미만, true/false는 각 1줄입니다.
애플릿 구현 분석 (cat_main 예시)
BusyBox 애플릿이 어떻게 libbb의 공유 함수를 활용하여 극도로 간결하게 구현되는지,
cat 애플릿의 실제 소스 코드를 분석합니다
(소스: coreutils/cat.c):
/* coreutils/cat.c — BusyBox cat 구현 (핵심부 단순화) */
//applet:IF_CAT(APPLET(cat, BB_DIR_BIN, BB_SUID_DROP))
//usage:#define cat_trivial_usage
//usage: "[-nbvteA] [FILE]..."
int cat_main(int argc UNUSED_PARAM, char **argv)
{
struct stat st;
unsigned flags;
/* getopt32(): libbb 공유 옵션 파서
* 한 줄로 -n, -b, -v, -t, -e, -A 옵션을 모두 파싱 */
flags = getopt32(argv, "nbvteA");
argv += optind;
if (!*argv) *--argv = (char*)"-"; /* 인자 없으면 stdin */
do {
/* xopen_stdin(): libbb 공유 함수
* "-"이면 stdin, 아니면 open() + 에러 시 자동 종료 */
int fd = open_or_warn_stdin(*argv);
if (fd < 0) { argv++; continue; }
for (;;) {
/* bb_copyfd_eof(): libbb 핵심 공유 함수
* fd → stdout 전체 복사. 모든 에러 처리 내장 */
if (bb_copyfd_eof(fd, STDOUT_FILENO) == -1)
break;
}
close(fd);
} while (*++argv);
return EXIT_SUCCESS;
}
이 구현에서 볼 수 있는 BusyBox 설계 패턴:
getopt32()— libbb의 통합 옵션 파서. GNU getopt_long()보다 훨씬 작고, 한 줄로 모든 옵션 정의open_or_warn_stdin()— 파일 열기 + 에러 메시지 + stdin 폴백을 하나의 함수로 처리bb_copyfd_eof()— 파일 디스크립터 간 데이터 복사. cat, cp, tee 등 여러 애플릿이 공유- 매크로
//applet:— 빌드 시 자동으로 애플릿 테이블에 등록. 별도의 등록 코드 불필요 - 전체 cat 구현이 약 30줄 — GNU cat의 ~300줄 대비 1/10. libbb 공유 코드 덕분
애플릿 디스패치 메커니즘
BusyBox의 애플릿 디스패치는 빌드 시점에 생성되는 정렬된 테이블과 런타임 이진 탐색으로 구현됩니다.
빌드 시 include/applets.h의 매크로가 정렬된 애플릿 이름 테이블과 함수 포인터 배열을 생성합니다.
| 단계 | 함수/구조체 | 설명 |
|---|---|---|
| 1 | main() | 프로그램 진입점. argv[0] 추출 |
| 2 | lbb_prepare() | 시그널 핸들러, locale 등 초기 설정 |
| 3 | bb_basename() | argv[0]에서 경로 제거 ("/bin/ls" → "ls") |
| 4 | find_applet_by_name() | 정렬된 applet_names[]에서 이진 탐색. O(log n) 복잡도 |
| 5 | applet_main[idx]() | 인덱스로 함수 포인터 배열 접근 → ls_main() 등 호출 |
심볼릭 링크 메커니즘
BusyBox 설치 시 make install은 활성화된 모든 애플릿에 대해 심볼릭 링크를 생성합니다.
링크 방식은 세 가지가 있습니다:
| 방식 | 명령어 | 장점 | 단점 |
|---|---|---|---|
| 심볼릭 링크 | busybox --install -s | 표준적, 디버깅 용이 (ls -la로 확인 가능) | 각 링크가 inode 사용 |
| 하드 링크 | busybox --install | inode 절약, 원본 삭제 시에도 동작 | 파일시스템 경계 불가, 디버깅 어려움 |
| 직접 호출 | busybox ls -la | 링크 불필요 | 사용 불편, 스크립트 호환성 없음 |
# 심볼릭 링크 설치 예시
$ ls -la /bin/
lrwxrwxrwx 1 root root 7 Jan 1 00:00 ash -> busybox
lrwxrwxrwx 1 root root 7 Jan 1 00:00 cat -> busybox
lrwxrwxrwx 1 root root 7 Jan 1 00:00 ls -> busybox
lrwxrwxrwx 1 root root 7 Jan 1 00:00 sh -> busybox
-rwxr-xr-x 1 root root 1089568 Jan 1 00:00 busybox
# 직접 호출 vs 심볼릭 링크 호출
$ busybox ls /tmp # 직접 호출: busybox 뒤에 애플릿 이름
$ ls /tmp # 심볼릭 링크: /bin/ls → /bin/busybox
소스 코드 구조
BusyBox 소스 트리는 애플릿 카테고리별 디렉토리로 구성됩니다:
| 디렉토리 | 설명 | 주요 파일 |
|---|---|---|
applets/ | 진입점, 애플릿 테이블 생성 | applets.c, usage.c |
archival/ | 압축/아카이브 (tar, gzip, bzip2) | tar.c, gzip.c |
coreutils/ | 기본 유틸리티 (ls, cp, cat, echo) | ls.c, cp.c |
editors/ | 텍스트 편집 (vi, sed, awk) | vi.c, sed.c |
init/ | init 시스템, halt, reboot | init.c, halt.c |
libbb/ | 공용 라이브러리 (모든 애플릿이 공유) | xfuncs.c, messages.c |
networking/ | 네트워크 (ifconfig, wget, httpd) | wget.c, httpd.c |
shell/ | 셸 (ash, hush) | ash.c, hush.c |
sysklogd/ | 로깅 (syslogd, klogd) | syslogd.c |
util-linux/ | 시스템 유틸리티 (mount, fdisk, dmesg) | mount.c, switch_root.c |
include/ | 헤더 파일, 애플릿 매크로 정의 | applets.h, libbb.h |
빌드와 설정
빌드 환경 준비
BusyBox 빌드에 필요한 호스트 패키지:
# Ubuntu/Debian
sudo apt-get install -y build-essential gcc make libncurses-dev \
bzip2 wget git bc
# 크로스 컴파일용 (ARM64)
sudo apt-get install -y gcc-aarch64-linux-gnu
# 크로스 컴파일용 (RISC-V)
sudo apt-get install -y gcc-riscv64-linux-gnu
# 정적 링크용 (musl)
sudo apt-get install -y musl-tools
menuconfig 설정
BusyBox는 리눅스 커널과 동일한 Kconfig 시스템을 사용합니다.
make menuconfig로 ncurses 기반 설정 인터페이스에서 애플릿과 옵션을 선택합니다.
전체 설정 옵션 목록은 소스 트리의 Config.in 파일과
공식 매뉴얼에서 확인할 수 있습니다.
주요 설정 카테고리:
| 카테고리 | 주요 옵션 | 설명 |
|---|---|---|
| Settings | CONFIG_STATIC=y | 정적 링크. initramfs/임베디드에 필수 |
CONFIG_CROSS_COMPILER_PREFIX | 크로스 컴파일러 접두사 설정 | |
CONFIG_FEATURE_PREFER_APPLETS=y | 외부 명령 대신 내장 애플릿 우선 사용. 크기 절약 | |
CONFIG_FEATURE_SH_STANDALONE=y | 셸이 외부 프로그램 대신 busybox 애플릿을 직접 호출 | |
| Shells | CONFIG_ASH=y | Almquist Shell. POSIX 호환 경량 셸 |
CONFIG_HUSH=y | Hush Shell. NOMMU 환경용 셸 | |
| Init Utilities | CONFIG_INIT=y | PID 1 init 프로세스 |
CONFIG_FEATURE_INIT_SCTTY=y | init이 제어 터미널을 설정 | |
| Linux System | CONFIG_MDEV=y | 경량 디바이스 관리자 |
CONFIG_SWITCH_ROOT=y | initramfs → 실제 루트 전환 | |
| Networking | CONFIG_UDHCPC=y | DHCP 클라이언트 |
CONFIG_HTTPD=y | 경량 HTTP 서버 |
# menuconfig 설정 흐름
make defconfig # 기본 설정 생성
make menuconfig # 대화형 설정 수정
# .config에서 직접 수정도 가능
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
make oldconfig # 의존성 자동 해결
defconfig와 최소 설정
BusyBox는 두 가지 기본 설정 프리셋을 제공합니다:
| 설정 | 명령어 | 애플릿 수 | 결과 크기 | 용도 |
|---|---|---|---|---|
| defconfig | make defconfig | ~300 | ~1.1 MB (정적) | 범용 임베디드 시스템 |
| allnoconfig | make allnoconfig | 0 | N/A | 최소 설정 시작점 (수동 선택) |
# 최소 initramfs용 설정 예시 (allnoconfig + 수동 선택)
make allnoconfig
# 필수 애플릿만 활성화
cat >> .config <<'EOF'
CONFIG_ASH=y
CONFIG_INIT=y
CONFIG_MOUNT=y
CONFIG_UMOUNT=y
CONFIG_MKDIR=y
CONFIG_MKNOD=y
CONFIG_CAT=y
CONFIG_ECHO=y
CONFIG_LS=y
CONFIG_SWITCH_ROOT=y
CONFIG_STATIC=y
EOF
make oldconfig # 의존성 해결
make -j$(nproc)
정적 링크 vs 동적 링크
| 방식 | 설정 | 크기 | 장점 | 단점 |
|---|---|---|---|---|
| 정적 링크 | CONFIG_STATIC=y | ~1.1 MB | 독립 실행, initramfs에 이상적, 라이브러리 의존성 없음 | 더 큰 바이너리, 보안 패치 시 재빌드 필요 |
| 동적 링크 (glibc) | (기본값) | ~700 KB + libc | 공유 라이브러리로 전체 시스템 크기 절약 | glibc 의존, initramfs에 부적합 |
| 정적 링크 (musl) | CC=musl-gcc CONFIG_STATIC=y | ~800 KB | glibc보다 작은 정적 바이너리, 라이선스 깔끔 (MIT) | 일부 glibc 확장 미지원 |
# musl로 정적 빌드 (가장 작은 정적 바이너리)
make defconfig
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config
make CC=musl-gcc -j$(nproc)
strip busybox
ls -lh busybox # ~800KB
크로스 컴파일
임베디드 타겟용 BusyBox 크로스 컴파일:
# ARM64 (aarch64) 크로스 컴파일
make defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
CONFIG_STATIC=y -j$(nproc)
# RISC-V 64bit 크로스 컴파일
make defconfig
make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- \
CONFIG_STATIC=y -j$(nproc)
# ARM 32bit (hard float) 크로스 컴파일
make defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
CONFIG_STATIC=y -j$(nproc)
# 결과 확인
file busybox
# busybox: ELF 64-bit LSB executable, ARM aarch64, statically linked, ...
BR2_PACKAGE_BUSYBOX=y와 BR2_PACKAGE_BUSYBOX_CONFIG로 설정합니다.
핵심 애플릿 카테고리
셸 (ash/hush)
BusyBox는 두 가지 셸을 제공합니다:
| 셸 | 크기 | 특징 | 환경 |
|---|---|---|---|
| ash | ~60 KB | POSIX 호환, 대부분의 sh 스크립트 실행 가능. bash 일부 확장 지원 ($(()), local) | MMU 환경 (일반 임베디드) |
| hush | ~40 KB | 최소 POSIX 셸. 히어독(heredoc), 파이프 지원 | NOMMU 환경 (Cortex-M, uClinux) |
ash vs bash 주요 차이점:
| 기능 | bash | ash (BusyBox) |
|---|---|---|
| 배열(Array) | arr=(a b c); echo ${arr[1]} | 미지원. set -- a b c; echo $2 로 대체 |
| 확장 테스트 | [[ $x =~ regex ]] | 미지원. echo "$x" | grep -E 'regex' |
| 프로세스 치환 | diff <(cmd1) <(cmd2) | 미지원. 임시 파일 사용 |
| 연관 배열 | declare -A map | 미지원 |
| 문자열 조작 | ${var^^} (대문자 변환) | 미지원. echo "$var" | tr a-z A-Z |
| 산술 확장 | $(( )) | 지원 |
| 함수/local | local var=val | 지원 |
| here string | <<< "text" | 지원 (CONFIG_ASH_BASH_COMPAT) |
파일 유틸리티
BusyBox의 coreutils 애플릿은 GNU coreutils 대비 옵션이 축소되어 있습니다. 자주 사용하는 옵션은 대부분 지원하지만, 덜 사용되는 옵션은 빌드 크기를 줄이기 위해 생략됩니다:
| 애플릿 | GNU 옵션 수 | BusyBox 옵션 수 | 누락된 주요 옵션 |
|---|---|---|---|
ls | ~60 | ~25 | --color=auto (별도 CONFIG), --context |
cp | ~30 | ~15 | --reflink, --sparse |
find | ~50 | ~20 | -printf (별도 CONFIG), -regextype |
sed | ~15 | ~8 | -E/--regexp-extended (별도 CONFIG) |
awk | gawk 전체 | 기본 POSIX | 대부분의 gawk 확장 (FPAT, @include 등) |
네트워크
BusyBox는 임베디드 시스템에 필요한 기본 네트워크 도구를 제공합니다:
| 애플릿 | 설명 | 주요 옵션 |
|---|---|---|
ifconfig | 네트워크 인터페이스 설정 | ip 주소 설정, up/down |
ip | iproute2 경량 구현 | addr, link, route 서브커맨드 |
wget | HTTP/HTTPS 다운로더 | -O, -q, SSL 지원 (CONFIG 의존) |
httpd | 경량 HTTP 서버 | CGI 지원, 디렉토리 리스팅 |
udhcpc | DHCP 클라이언트 | 스크립트 기반 설정 적용 |
udhcpd | DHCP 서버 | 임베디드 라우터용 경량 DHCP |
telnetd | Telnet 서버 | 디버깅용 원격 접속 |
ntpd | NTP 클라이언트/서버 | 시간 동기화 |
init 시스템 애플릿
BusyBox init 관련 애플릿:
| 애플릿 | 설명 |
|---|---|
init | PID 1 프로세스. /etc/inittab을 파싱하여 시스템 초기화 수행 |
halt | 시스템 정지. init에 SIGUSR1 전송 |
poweroff | 시스템 전원 끄기. init에 SIGUSR2 전송 |
reboot | 시스템 재부팅. init에 SIGTERM 전송 |
getty | 터미널 로그인 프롬프트 제공 |
모듈 유틸리티
커널 모듈 관리용 애플릿:
| 애플릿 | 설명 |
|---|---|
insmod | 커널 모듈 삽입 (.ko 파일 직접 로드) |
rmmod | 커널 모듈 제거 |
modprobe | 의존성을 해결하며 모듈 로드/언로드 |
depmod | 모듈 의존성 파일 (modules.dep) 생성 |
lsmod | 로드된 커널 모듈 목록 출력 |
modinfo | 모듈 정보 출력 |
시스템 유틸리티
| 애플릿 | 설명 | 비고 |
|---|---|---|
mount | 파일시스템 마운트 | NFS, CIFS 등 네트워크 FS도 지원 (CONFIG 의존) |
umount | 파일시스템 언마운트 | |
mdev | 경량 디바이스 관리자 | udev 대안. hotplug 이벤트 처리 |
switch_root | initramfs → 실제 루트 전환 | initramfs의 init 스크립트에서 사용 |
syslogd | 시스템 로그 데몬 | 원격 로깅 지원 |
klogd | 커널 로그 데몬 | /proc/kmsg 읽어서 syslogd에 전달 |
dmesg | 커널 링 버퍼 출력 | |
free | 메모리 사용량 출력 |
BusyBox init 시스템
inittab 구조
BusyBox init은 /etc/inittab 파일을 읽어 부팅 프로세스를 제어합니다.
형식은 sysvinit과 유사하지만 단순화되어 있습니다:
# /etc/inittab 형식:
# <id>:<runlevels>:<action>:<process>
#
# id - 터미널 식별자 (tty 번호 등, BusyBox에서는 무시 가능)
# runlevels - BusyBox init에서는 무시됨 (런레벨 미지원)
# action - 실행 방식 (sysinit, wait, once, respawn, askfirst 등)
# process - 실행할 명령어
# 시스템 초기화 (부팅 시 1회 실행, 완료까지 대기)
::sysinit:/etc/init.d/rcS
# 초기화 후 1회 실행
::wait:/bin/echo "System initialization complete"
# 콘솔 (사용자가 Enter 키를 누르면 셸 시작)
::askfirst:-/bin/sh
# 시리얼 콘솔 (재시작 반복)
ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100
# 시스템 종료 시 실행
::shutdown:/etc/init.d/rcK
::shutdown:/bin/umount -a -r
# Ctrl-Alt-Del 처리
::ctrlaltdel:/sbin/reboot
inittab 액션 유형:
| 액션 | 설명 | 사용 시점 |
|---|---|---|
| sysinit | 시스템 초기화 시 실행. 완료까지 대기 후 다음 진행 | rcS 스크립트 실행 |
| wait | sysinit 완료 후 실행. 완료까지 대기 | 추가 초기화 작업 |
| once | 1회 실행. 완료를 기다리지 않음 | 백그라운드 서비스 시작 |
| respawn | 프로세스 종료 시 자동 재시작 | getty, 데몬 프로세스 |
| askfirst | respawn과 유사하나 "Press Enter to activate" 메시지 표시 후 대기 | 디버깅 콘솔 |
| shutdown | 시스템 종료 시 실행 | 정리(cleanup) 작업 |
| restart | SIGQUIT 수신 시 실행 | init 재시작 |
| ctrlaltdel | Ctrl+Alt+Del 키 입력 시 실행 | 재부팅 |
/etc/inittab 파일이 존재하지 않으면 BusyBox init은 하드코딩된 기본값을 사용합니다.
inittab 없을 때 기본 동작
BusyBox init은 /etc/inittab을 찾지 못하면 init/init.c에 하드코딩된 기본 설정을 사용합니다.
이는 최소 initramfs 환경에서 별도의 inittab 없이도 시스템을 부팅할 수 있게 합니다:
# BusyBox init 하드코딩 기본값 (init/init.c에서 발췌)
# inittab 파일이 없을 때 자동 적용
::sysinit:/etc/init.d/rcS # 초기화 스크립트 실행
::askfirst:-/bin/sh # 콘솔 장치에서 셸 제공
tty2::askfirst:-/bin/sh # 가상 터미널 2
tty3::askfirst:-/bin/sh # 가상 터미널 3
tty4::askfirst:-/bin/sh # 가상 터미널 4
콘솔 장치는 /dev/console에서 자동으로 결정됩니다.
시리얼 콘솔(Serial Console) 환경(예: ttyS0)에서는 해당 장치에 대해 하나의 askfirst 항목만 생성됩니다.
명령어 앞의 하이픈(-/bin/sh)은 로그인 셸(Login Shell)을 의미하며, argv[0]이 -sh로 설정되어
/etc/profile과 ~/.profile을 읽습니다.
/etc/init.d/rcS 스크립트만 제공하면 기본 동작으로 충분합니다.
명시적 inittab은 getty 설정, 시리얼 콘솔 속도 지정, 또는 shutdown 처리가 필요할 때 추가합니다.
rcS 스크립트 실행 흐름
/etc/init.d/rcS는 초기화 스크립트를 순서대로 실행하는 마스터 스크립트입니다:
#!/bin/sh
# /etc/init.d/rcS — BusyBox 초기화 마스터 스크립트
# /etc/init.d/ 디렉토리의 S##name 스크립트를 번호 순서대로 실행
for script in /etc/init.d/S[0-9][0-9]*; do
[ -x "$script" ] || continue
case "$script" in
*.sh) . "$script" ;; # .sh 확장자는 source로 실행
*) "$script" start ;; # 나머지는 "start" 인자로 실행
esac
done
일반적인 초기화 스크립트 순서:
| 스크립트 | 역할 |
|---|---|
S01sysfs | 가상 파일시스템 마운트 (/proc, /sys, /dev) |
S02hostname | 호스트명 설정 |
S05modules | 커널 모듈 로드 |
S10mdev | mdev 디바이스 관리자 시작 |
S20network | 네트워크 인터페이스 설정, udhcpc 시작 |
S30syslogd | syslogd/klogd 시작 |
S40httpd | 웹 서버 시작 (선택) |
S99local | 사용자 정의 초기화 |
init 내부 동작
BusyBox init의 핵심 코드는 init/init.c 약 900줄로 구성됩니다.
init_main() 함수가 PID 1로서 다음 순서로 실행됩니다:
- 콘솔(Console) 설정 —
/dev/console을 열고,setsid()로 새 세션 생성, STDIN/STDOUT/STDERR을 콘솔에 연결 - 시그널 핸들러(Signal Handler) 등록 —
SIGHUP,SIGQUIT,SIGUSR1/2,SIGTERM,SIGINT를 블록하고sigtimedwait()방식으로 처리 (비동기 안전성(Async-Signal Safety) 확보) - 환경 변수 초기화 —
PATH=/sbin:/usr/sbin:/bin:/usr/bin설정 parse_inittab()—/etc/inittab을 파싱하여init_action연결 리스트(Linked List) 구성. 파일이 없으면 기본값 사용run_actions(SYSINIT)— sysinit 타입 액션 실행,waitfor_pid()로 각 프로세스 완료까지 블로킹(Blocking) 대기run_actions(WAIT)— wait 타입 액션 실행, 완료까지 대기run_actions(ONCE)— once 타입 액션 실행 (대기하지 않음, 백그라운드로 진행)- 메인 루프(Main Loop) 진입 — respawn/askfirst 프로세스를 관리하는 무한 루프
메인 루프는 다음 4단계를 반복합니다:
run_actions(RESPAWN)— 종료된 respawn 프로세스를 재시작run_actions(ASKFIRST)— 종료된 askfirst 프로세스를 재시작 (Enter 키 대기 포함)wpid = waitpid(-1, &status, WNOHANG)— 자식 프로세스(Child Process) 종료 상태 수집 (좀비 리핑(Zombie Reaping))check_delayed_sigs()— 블록된 시그널 확인 및 처리 →sleep(1)후 반복
init_action 구조체는 inittab의 각 항목을 메모리에 표현합니다:
/* BusyBox init/init.c — init_action 구조체 */
struct init_action {
struct init_action *next; /* 연결 리스트 다음 노드 */
pid_t pid; /* 실행 중인 프로세스 PID (0이면 미실행) */
uint8_t action_type; /* SYSINIT, WAIT, ONCE, RESPAWN, ASKFIRST 등 */
char terminal[CONSOLE_NAME_SIZE]; /* 터미널 장치명 (예: "ttyS0") */
char command[256]; /* 실행할 명령어 */
};
waitfor_pid()는 특정 PID가 종료될 때까지 블로킹하는 래퍼(Wrapper) 함수로,
SYSINIT과 WAIT 액션에서 사용됩니다.
메인 루프의 waitpid(-1, WNOHANG)는 논블로킹(Non-Blocking)으로 모든 종료된 자식을 수집합니다.
PID 1 역할과 시그널 처리
PID 1인 BusyBox init은 두 가지 핵심 역할을 수행합니다: (1) 시그널(Signal)을 통한 시스템 상태 전환(종료, 재부팅, 설정 재로드), (2) 고아 프로세스(Orphan Process) 입양(Adoption)과 좀비 리핑(Zombie Reaping).
시그널 매핑
| 시그널 | 발신 명령어 | init 동작 |
|---|---|---|
| SIGHUP | — | /etc/inittab 다시 읽기 (parse_inittab() 재호출) |
| SIGQUIT | — | restart 액션 실행 (init 자체 재시작) |
| SIGUSR1 | halt | shutdown 액션 → SIGTERM → SIGKILL → halt() |
| SIGUSR2 | poweroff | shutdown 액션 → SIGTERM → SIGKILL → poweroff() |
| SIGTERM | reboot | shutdown 액션 → SIGTERM → SIGKILL → reboot() |
| SIGINT | Ctrl+Alt+Del (커널) | ctrlaltdel 액션 실행 |
| SIGCHLD | 자식 종료 | waitpid()로 수집, respawn 대상이면 재시작 |
종료 시퀀스(Shutdown Sequence)
halt, poweroff, reboot 명령은 각각 대응하는 시그널을 PID 1에 전송합니다.
BusyBox init은 다음 6단계로 시스템을 정리합니다:
- shutdown 액션 실행 —
/etc/init.d/rcK,umount -a -r등 inittab의 shutdown 항목 - SIGTERM 전송 — 모든 프로세스에 종료 요청 (
kill(-1, SIGTERM)) - 1초 대기 — 프로세스가 정상 종료할 시간 제공
- SIGKILL 전송 — 남은 프로세스 강제 종료 (
kill(-1, SIGKILL)) sync()호출 — 파일시스템 버퍼를 디스크에 기록- 시스템 콜 실행 —
reboot(RB_HALT_SYSTEM),reboot(RB_POWER_OFF), 또는reboot(RB_AUTOBOOT)
좀비 리핑(Zombie Reaping)
리눅스에서 부모 프로세스가 먼저 종료되면 자식은 PID 1에 입양됩니다.
PID 1은 waitpid(-1, &status, WNOHANG)로 모든 종료된 자식의 상태를 수집해야 합니다.
이것이 PID 1만의 특수한 의무이며, 수행하지 않으면 좀비 프로세스(Zombie Process)가 프로세스 테이블에 누적됩니다.
BusyBox init은 메인 루프에서 이를 자동 처리하며, 수집된 PID가 respawn/askfirst 목록에 있으면 해당 프로세스를 재시작합니다.
SIGHUP: inittab 재로드
SIGHUP 수신 시 parse_inittab()을 다시 호출하여 inittab을 재파싱합니다.
새로 추가된 항목은 다음 루프 반복에서 실행되고, 삭제된 항목은 연결 리스트에서 제거됩니다.
단, 이미 실행 중인 프로세스는 종료하지 않습니다 — 해당 프로세스가 자연스럽게 종료된 후 재시작하지 않는 방식으로 처리됩니다.
실전 inittab 예제
예제 1: 최소 임베디드 시스템
rcS 스크립트 없이 inittab만으로 최소 초기화를 수행합니다. initramfs 디버깅에 적합합니다:
# /etc/inittab — 최소 임베디드 (rcS 없이 직접 초기화)
::sysinit:/bin/mount -t proc proc /proc
::sysinit:/bin/mount -t sysfs sys /sys
::sysinit:/bin/mount -t devtmpfs dev /dev
::sysinit:/bin/echo /sbin/mdev > /proc/sys/kernel/hotplug
::sysinit:/sbin/mdev -s
::askfirst:-/bin/sh
::shutdown:/bin/umount -a -r
::ctrlaltdel:/sbin/reboot
예제 2: 네트워크 어플라이언스
SSH 서버(dropbear)는 once로 1회 시작하고, 웹 서버는 respawn으로 크래시 시 자동 복구합니다:
# /etc/inittab — 네트워크 어플라이언스
::sysinit:/etc/init.d/rcS
::wait:/sbin/ifconfig eth0 192.168.1.1 netmask 255.255.255.0 up
# SSH: 1회 시작 (자체 데몬화)
::once:/usr/sbin/dropbear -R
# 웹 서버: 포그라운드 실행, 종료 시 자동 재시작
::respawn:/usr/sbin/lighttpd -D -f /etc/lighttpd.conf
# 시리얼 콘솔
ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100
::shutdown:/etc/init.d/rcK
::ctrlaltdel:/sbin/reboot
예제 3: 컨테이너 init
컨테이너에서 단일 애플리케이션을 PID 1로 감시합니다. respawn이 크래시 복구를 제공합니다:
# /etc/inittab — 컨테이너 init
::sysinit:/bin/mount -t proc proc /proc
::respawn:/usr/bin/my-app --foreground
::shutdown:/bin/kill -TERM $(cat /var/run/my-app.pid)
예제 4: 개발/디버깅 환경
커널 로그 레벨을 최대로 올리고, 시리얼과 콘솔 양쪽에서 접근 가능한 환경입니다:
# /etc/inittab — 개발/디버깅
::sysinit:/etc/init.d/rcS
::wait:/bin/echo "=== Debug Console Ready ==="
::wait:/bin/dmesg -n 8
# 시리얼 콘솔 (respawn으로 세션 종료 시 재시작)
ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100
# 로컬 콘솔 (askfirst로 Enter 대기)
::askfirst:-/bin/sh
::shutdown:/bin/sync
::ctrlaltdel:/sbin/reboot
"process 'xxx' (pid NNN) exited. Scheduling for restart." 메시지가 반복되면
해당 프로세스의 설정을 점검하세요.
init 비교
| 항목 | BusyBox init | sysvinit | systemd |
|---|---|---|---|
| 크기 | ~20 KB (BusyBox 내장) | ~500 KB | ~10 MB+ |
| 설정 | /etc/inittab | /etc/inittab + /etc/rc.d/ | /etc/systemd/ (유닛 파일) |
| 런레벨 | 미지원 | 0~6 런레벨 | 타겟(target) 기반 |
| 병렬 시작 | 미지원 (순차 실행) | 미지원 | 지원 (의존성 그래프) |
| 서비스 관리 | 수동 (kill, 스크립트) | service 명령 | systemctl |
| 로깅 | syslogd (선택) | syslog | journald (바이너리 로그) |
| 소켓 활성화 | 미지원 | 미지원 | 지원 |
| cgroup 관리 | 미지원 | 미지원 | 지원 |
| 대상 환경 | 임베디드, initramfs | 레거시 서버 | 데스크톱, 서버 |
각 init 시스템의 상세한 설명은 systemd 종합 가이드와 SysVinit 종합 가이드를 참고하세요.
부팅 흐름
initramfs와 임베디드 리눅스 활용
initramfs 생성 절차
BusyBox 기반 initramfs는 임베디드 리눅스와 커널 디버깅에서 가장 많이 사용되는 루트 파일시스템 형태입니다. initramfs의 동작 원리는 커널 공식 문서에서 설명합니다:
# 1. 디렉토리 구조 생성
mkdir -p initramfs/{bin,sbin,etc,proc,sys,dev,tmp,mnt,root}
mkdir -p initramfs/usr/{bin,sbin}
mkdir -p initramfs/etc/init.d
mkdir -p initramfs/var/{log,run}
# 2. BusyBox 정적 빌드 복사 및 설치
cp busybox initramfs/bin/
chmod +x initramfs/bin/busybox
cd initramfs
bin/busybox --install -s bin/
bin/busybox --install -s sbin/
bin/busybox --install -s usr/bin/
bin/busybox --install -s usr/sbin/
# 3. 필수 디바이스 노드 생성 (또는 devtmpfs 사용)
sudo mknod -m 622 dev/console c 5 1
sudo mknod -m 666 dev/null c 1 3
sudo mknod -m 666 dev/zero c 1 5
sudo mknod -m 444 dev/random c 1 8
sudo mknod -m 444 dev/urandom c 1 9
# 4. cpio 아카이브 생성
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
cd ..
init 스크립트 패턴
initramfs의 /init 스크립트는 커널이 가장 먼저 실행하는 사용자 공간 프로그램입니다.
용도에 따라 다양한 패턴이 있으며, 모든 패턴은 POSIX sh 호환으로 작성해야 합니다:
패턴 1: 단순 디버깅용 (셸로 진입)
커널 개발과 디버깅 시 가장 자주 사용하는 패턴입니다. 최소한의 가상 파일시스템만 마운트하고 셸 프롬프트를 제공합니다:
#!/bin/sh
# /init — 단순 initramfs init (커널 디버깅용)
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
# 커널 정보 출력
echo "================================================="
echo " BusyBox initramfs booted successfully!"
echo " Kernel: $(uname -r)"
echo " cmdline: $(cat /proc/cmdline)"
echo "================================================="
exec /bin/sh
패턴 2: 실제 루트로 전환 (switch_root)
프로덕션 임베디드 시스템에서 사용하는 패턴입니다. initramfs에서 하드웨어를 초기화한 뒤 실제 루트 파일시스템으로 전환합니다:
#!/bin/sh
# /init — switch_root 패턴 (프로덕션용)
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
# mdev로 디바이스 노드 동적 생성
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
# 커널 파라미터에서 루트 장치 추출
ROOT=$(cat /proc/cmdline | sed 's/.*root=\([^ ]*\).*/\1/')
# 루트 장치가 나타날 때까지 대기 (USB 등 느린 장치)
RETRY=0
while [ ! -b "$ROOT" ] && [ $RETRY -lt 30 ]; do
sleep 1
RETRY=$((RETRY + 1))
mdev -s
done
if [ ! -b "$ROOT" ]; then
echo "FATAL: Root device $ROOT not found!"
exec /bin/sh # 복구 셸 제공
fi
# 실제 루트 파일시스템 마운트
mount -o ro "$ROOT" /mnt
# 정리 후 switch_root
umount /proc /sys /dev 2>/dev/null
exec switch_root /mnt /sbin/init
패턴 3: 완전한 임베디드 루트 (switch_root 없이)
initramfs 자체가 최종 루트 파일시스템인 경우입니다. 플래시 없는 네트워크 부팅이나 read-only 장비에서 사용합니다:
#!/bin/sh
# /init — initramfs가 최종 루트 (inittab 기반)
# 가상 파일시스템 마운트
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mkdir -p /dev/pts /dev/shm
mount -t devpts none /dev/pts
mount -t tmpfs none /dev/shm
mount -t tmpfs none /tmp
mount -t tmpfs none /var
# 필요한 디렉토리 생성 (tmpfs 위)
mkdir -p /var/log /var/run /var/lock
# mdev 시작
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
# 호스트명 설정
hostname "busybox-system"
# syslogd/klogd 시작
syslogd -C256 # 256KB 원형 버퍼
klogd
# BusyBox init으로 전환 (inittab 사용)
exec /sbin/init
완전한 루트 파일시스템 구축
BusyBox 기반으로 inittab, rcS, 네트워크, 로깅까지 갖춘 완전한 임베디드 루트 파일시스템을 단일 스크립트로 구축하는 레시피입니다:
#!/bin/sh
# build-rootfs.sh — BusyBox 기반 완전한 루트 파일시스템 구축 스크립트
set -e
ROOTFS="rootfs"
BUSYBOX="./busybox" # 정적 빌드된 BusyBox 바이너리
echo "=== 1. 디렉토리 구조 생성 ==="
rm -rf "$ROOTFS"
mkdir -p "$ROOTFS"/{bin,sbin,etc/init.d,proc,sys,dev,tmp,mnt,root}
mkdir -p "$ROOTFS"/usr/{bin,sbin}
mkdir -p "$ROOTFS"/var/{log,run,lock,spool}
mkdir -p "$ROOTFS"/usr/share/udhcpc
echo "=== 2. BusyBox 설치 ==="
cp "$BUSYBOX" "$ROOTFS/bin/busybox"
chmod 755 "$ROOTFS/bin/busybox"
chroot "$ROOTFS" /bin/busybox --install -s
echo "=== 3. 기본 설정 파일 생성 ==="
# /etc/inittab
cat > "$ROOTFS/etc/inittab" <<'EOF'
::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty -L ttyS0 115200 vt100
tty1::askfirst:-/bin/sh
::shutdown:/etc/init.d/rcK
::ctrlaltdel:/sbin/reboot
EOF
# /etc/init.d/rcS (마스터 초기화)
cat > "$ROOTFS/etc/init.d/rcS" <<'SCRIPT'
#!/bin/sh
for s in /etc/init.d/S[0-9][0-9]*; do
[ -x "$s" ] && "$s" start
done
SCRIPT
chmod +x "$ROOTFS/etc/init.d/rcS"
# /etc/init.d/rcK (종료)
cat > "$ROOTFS/etc/init.d/rcK" <<'SCRIPT'
#!/bin/sh
for s in $(ls -r /etc/init.d/S[0-9][0-9]*); do
[ -x "$s" ] && "$s" stop
done
SCRIPT
chmod +x "$ROOTFS/etc/init.d/rcK"
# S01sysfs — 가상 파일시스템 마운트
cat > "$ROOTFS/etc/init.d/S01sysfs" <<'SCRIPT'
#!/bin/sh
case "$1" in
start)
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mkdir -p /dev/pts /dev/shm
mount -t devpts none /dev/pts
mount -t tmpfs none /dev/shm
mount -t tmpfs none /tmp
mount -t tmpfs none /var
mkdir -p /var/log /var/run /var/lock
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
;;
esac
SCRIPT
chmod +x "$ROOTFS/etc/init.d/S01sysfs"
# S20network — 네트워크 설정
cat > "$ROOTFS/etc/init.d/S20network" <<'SCRIPT'
#!/bin/sh
case "$1" in
start) ifconfig lo 127.0.0.1 up
ifconfig eth0 up
udhcpc -i eth0 -s /usr/share/udhcpc/default.script -b ;;
stop) ifconfig eth0 down ;;
esac
SCRIPT
chmod +x "$ROOTFS/etc/init.d/S20network"
# udhcpc default.script
cat > "$ROOTFS/usr/share/udhcpc/default.script" <<'SCRIPT'
#!/bin/sh
case "$1" in
bound|renew)
ifconfig "$interface" "$ip" netmask "$subnet"
[ -n "$router" ] && route add default gw "$router"
[ -n "$dns" ] && echo "nameserver $dns" > /etc/resolv.conf ;;
deconfig) ifconfig "$interface" 0.0.0.0 ;;
esac
SCRIPT
chmod +x "$ROOTFS/usr/share/udhcpc/default.script"
# /etc/passwd, /etc/group, /etc/shadow
echo "root:x:0:0:root:/root:/bin/sh" > "$ROOTFS/etc/passwd"
echo "root:x:0:" > "$ROOTFS/etc/group"
echo "root::0:0:99999:7:::" > "$ROOTFS/etc/shadow"
chmod 600 "$ROOTFS/etc/shadow"
# /etc/hostname, /etc/hosts
echo "busybox-dev" > "$ROOTFS/etc/hostname"
echo "127.0.0.1 localhost busybox-dev" > "$ROOTFS/etc/hosts"
# /etc/profile
cat > "$ROOTFS/etc/profile" <<'EOF'
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
export PS1='[\u@\h \W]\$ '
alias ll='ls -la'
EOF
echo "=== 4. cpio 아카이브 생성 ==="
cd "$ROOTFS"
find . | cpio -o -H newc | gzip > ../rootfs.cpio.gz
cd ..
echo "완료: rootfs.cpio.gz ($(du -h rootfs.cpio.gz | cut -f1))"
qemu-system-x86_64 -kernel bzImage -initrd rootfs.cpio.gz -append "console=ttyS0" -nographic로
네트워크, 로깅, 로그인 프롬프트가 있는 완전한 BusyBox 시스템을 테스트할 수 있습니다.
QEMU 테스트
# x86_64 테스트
qemu-system-x86_64 -kernel arch/x86/boot/bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0 nokaslr" -nographic -m 256M
# ARM64 (aarch64) 테스트
qemu-system-aarch64 -M virt -cpu cortex-a57 \
-kernel Image -initrd initramfs.cpio.gz \
-append "console=ttyAMA0" -nographic -m 512M
# RISC-V 64bit 테스트
qemu-system-riscv64 -M virt \
-kernel Image -initrd initramfs.cpio.gz \
-append "console=ttyS0" -nographic -m 256M
커널 내장 initramfs (CONFIG_INITRAMFS_SOURCE)
외부 -initrd 파일 대신 커널 이미지 안에 BusyBox initramfs를 직접 내장할 수 있습니다.
이 방식은 부트로더가 initrd 로딩을 지원하지 않는 환경이나, 단일 커널 이미지로 부팅해야 하는 경우에 유용합니다.
커널의 CONFIG_INITRAMFS_SOURCE 옵션을 사용합니다:
# 방법 1: 디렉토리 경로 지정 — 커널 빌드 시 자동 cpio 생성
# 커널 .config 에 설정:
CONFIG_INITRAMFS_SOURCE="/path/to/busybox-rootfs"
CONFIG_INITRAMFS_COMPRESSION_GZIP=y
# 방법 2: cpio 아카이브 직접 지정
CONFIG_INITRAMFS_SOURCE="/path/to/initramfs.cpio"
# 방법 3: gen_init_cpio 스펙 파일 사용 (가장 정밀한 제어)
# initramfs_spec.txt:
dir /bin 0755 0 0
dir /sbin 0755 0 0
dir /etc 0755 0 0
dir /proc 0755 0 0
dir /sys 0755 0 0
dir /dev 0755 0 0
file /bin/busybox /path/to/busybox 0755 0 0
slink /bin/sh busybox 0777 0 0
slink /sbin/init ../bin/busybox 0777 0 0
file /init /path/to/init.sh 0755 0 0
nod /dev/console 0600 0 0 c 5 1
nod /dev/null 0666 0 0 c 1 3
# 커널 .config 에 설정:
CONFIG_INITRAMFS_SOURCE="initramfs_spec.txt"
| 방법 | CONFIG_INITRAMFS_SOURCE 값 | 장점 | 단점 |
|---|---|---|---|
| 디렉토리 | /path/to/rootfs/ | 직관적, 파일 추가/수정 용이 | 파일 소유권/권한이 호스트에 의존 (root 필요할 수 있음) |
| cpio 아카이브 | /path/to/initramfs.cpio | 미리 생성된 아카이브 재사용 | 수정 시 아카이브 재생성 필요 |
| 스펙 파일 | initramfs_spec.txt | uid/gid/권한 정밀 제어, root 불필요 | 파일 추가 시 스펙 수동 편집 |
-initrd 방식이 더 편리하고, 최종 프로덕션에서 내장 방식을 사용하는 것이 일반적입니다.
빌드 시스템 통합
실제 임베디드 프로젝트에서는 BusyBox를 직접 빌드하기보다 빌드 시스템을 통해 통합합니다:
| 빌드 시스템 | BusyBox 설정 방법 | 설정 파일 |
|---|---|---|
| Buildroot | make busybox-menuconfig | package/busybox/busybox.config |
| OpenWrt | 기본 포함, make menuconfig → Utilities | package/utils/busybox/config/ |
| Yocto | CORE_IMAGE_EXTRA_INSTALL += "busybox" | meta/recipes-core/busybox/ |
# Buildroot에서 BusyBox 설정 변경
cd buildroot-2024.02
make busybox-menuconfig # BusyBox 전용 menuconfig
make # 전체 시스템 빌드 (BusyBox 포함)
컨테이너 환경 활용
BusyBox는 컨테이너 베이스 이미지로도 널리 사용됩니다. Docker Hub의 공식 BusyBox 이미지는 약 1.2MB로, 최소한의 셸 환경이 필요한 유틸리티 컨테이너에 적합합니다:
| 베이스 이미지 | 크기 | 셸 | 패키지 관리자 | 용도 |
|---|---|---|---|---|
busybox:latest | ~1.2 MB | ash | 없음 | 최소 유틸리티 컨테이너 |
busybox:musl | ~1.4 MB | ash | 없음 | musl 기반 (Alpine 호환) |
alpine:latest | ~7 MB | ash (BusyBox) | apk | 경량 리눅스 배포판 |
ubuntu:latest | ~78 MB | bash | apt | 범용 (비교 기준) |
scratch | 0 MB | 없음 | 없음 | 정적 바이너리 전용 |
Alpine Linux는 BusyBox + musl libc + apk 패키지 관리자로 구성된 경량 리눅스 배포판입니다. Docker 컨테이너 베이스로 사실상 표준이 되었으며, BusyBox의 가장 성공적인 활용 사례입니다. Alpine의 BusyBox 패키징은 Alpine 패키지 저장소에서 확인할 수 있습니다:
# BusyBox 기반 최소 컨테이너
docker run -it busybox:latest sh
/ # busybox --list | wc -l # 포함된 애플릿 수 확인
396
# Alpine Linux (BusyBox + musl + apk)
docker run -it alpine:latest sh
/ # cat /etc/alpine-release
3.20.0
/ # which ls
/bin/ls
/ # ls -la /bin/ls
lrwxrwxrwx 1 root root 12 ... /bin/ls -> /bin/busybox
# Dockerfile: BusyBox 기반 경량 이미지
# FROM busybox:musl
# COPY my-static-binary /usr/local/bin/app
# CMD ["/usr/local/bin/app"]
크기 최적화와 성능
크기 비교
BusyBox의 핵심 장점은 극적인 크기 절감입니다. 동일한 기능을 제공하는 GNU 유틸리티 대비 10~75배 작은 바이너리를 생성합니다. 아래 다이어그램은 각 구성별 크기를 시각적으로 비교합니다:
| 구성 | 크기 | 애플릿 수 | 비고 |
|---|---|---|---|
| BusyBox defconfig (정적, glibc) | ~1.1 MB | ~300 | 범용 임베디드 |
| BusyBox defconfig (정적, musl) | ~800 KB | ~300 | musl로 크기 절감 |
| BusyBox defconfig (동적, glibc) | ~700 KB + libc | ~300 | libc 공유 시 |
| BusyBox 최소 설정 (정적, musl) | ~200 KB | ~20 | initramfs 최소 |
| BusyBox + strip + UPX | ~300 KB | ~300 | 압축된 defconfig |
| GNU coreutils + bash + util-linux | ~15 MB+ | ~200 | 개별 바이너리 합산 |
| toybox (defconfig) | ~800 KB | ~230 | BSD 라이선스 대안 |
크기 줄이기 기법
| 기법 | 절감 효과 | 방법 |
|---|---|---|
| 불필요 애플릿 제거 | 최대 80% | make allnoconfig 후 필요한 것만 활성화 |
| musl libc 사용 | ~30% | make CC=musl-gcc CONFIG_STATIC=y |
| FEATURE_PREFER_APPLETS | ~5~10% | 외부 프로그램 대신 내장 애플릿 우선. fork/exec 오버헤드 제거 |
| FEATURE_SH_STANDALONE | ~5% | 셸이 PATH 탐색 대신 내장 애플릿 직접 호출 |
| strip | ~20% | strip busybox — 디버그 심볼 제거 |
| UPX 압축 | ~60% | upx --best busybox — 런타임 압축 해제 (부팅 시간 증가) |
| -Os 최적화 | ~10% | 기본 적용됨. -O2보다 크기 우선 |
# 최소 크기 빌드 예시
make allnoconfig
# 필수 애플릿만 선택 (.config 편집 또는 menuconfig)
make CC=musl-gcc CONFIG_STATIC=y -j$(nproc)
strip busybox
ls -lh busybox # ~200KB
# UPX 추가 압축 (선택)
upx --best busybox
ls -lh busybox # ~80KB (UPX 압축 후)
NOMMU 지원
MMU(Memory Management Unit)가 없는 프로세서(ARM Cortex-M, 일부 MIPS, Blackfin, ColdFire 등)에서는
fork() 시스템 콜을 사용할 수 없습니다. fork()는 프로세스 주소 공간을 복제하는데,
MMU 없이는 가상 메모리 매핑이 불가능하기 때문입니다.
BusyBox는 이러한 NOMMU 환경을 위해 vfork() 기반 동작을 지원합니다:
| 항목 | MMU 환경 (일반) | NOMMU 환경 |
|---|---|---|
| 프로세스 생성 | fork() + exec() | vfork() + exec() |
| 셸 | ash (권장) | hush (필수, ash는 fork 의존) |
| 파이프 | fork()로 자식 생성 | vfork() — 부모 일시 중단, 변수 공유 주의 |
| 바이너리 형식 | ELF (위치 독립 코드) | FLAT (bFLT), XIP (Execute In Place) |
| CONFIG 옵션 | (기본값) | CONFIG_NOMMU=y |
| 대상 프로세서 | Cortex-A, x86, RISC-V (S/U mode) | Cortex-M, Blackfin, ColdFire, RISC-V (M mode) |
| 메모리 보호 | 프로세스 간 격리 | 격리 없음 — 버그 시 시스템 전체 영향 |
/* MMU vs NOMMU 프로세스 생성 차이 */
/* MMU 환경: fork()가 주소 공간 복제 (COW) */
pid_t pid = fork();
if (pid == 0) {
/* 자식: 독립된 메모리 공간 */
exec("/bin/ls", ...);
}
/* NOMMU 환경: vfork()는 부모 스택 공유 */
pid_t pid = vfork();
if (pid == 0) {
/* 자식: 부모와 같은 메모리!
* exec() 또는 _exit()만 호출 가능
* 변수 수정, 함수 호출 금지 */
exec("/bin/ls", ...);
_exit(1); /* exec 실패 시 _exit 사용 (return 금지) */
}
/* 부모: vfork()는 자식이 exec/_exit할 때까지 블록됨 */
vfork()후 자식에서exec()또는_exit()만 호출 가능. 변수 수정, 함수 호출,return금지- hush 셸의 파이프 구현은 MMU 환경 대비 제한적 (중첩 파이프 제약)
- 프로세스 간 메모리 보호가 없으므로, 하나의 버그가 전체 시스템에 영향
- 동적 라이브러리 사용 불가 — 반드시 정적 링크 또는 XIP 사용
보안 고려사항
| 항목 | 설명 | 설정 |
|---|---|---|
| SUID | 일부 애플릿(passwd, su, ping)은 SUID 비트 필요 | CONFIG_FEATURE_SUID — SUID 실행 시 최소 권한으로 전환 |
| SELinux | SELinux 보안 컨텍스트 지원 | CONFIG_SELINUX=y — libselinux 의존 |
| 권한 분리 | 애플릿별 SUID 필요 여부 지정 | CONFIG_FEATURE_INDIVIDUAL=y — 애플릿별 개별 바이너리 생성 |
| 비밀번호 해싱 | SHA-256/SHA-512 해시 지원 | CONFIG_SHA256_HWACCEL 등 |
- 불필요한 애플릿(telnetd, ftpd 등)은 반드시 비활성화
- 기본 비밀번호를 설정하지 않은 채 getty/login을 활성화하지 말 것
CONFIG_FEATURE_SUID_CONFIG=y로 SUID 애플릿을 명시적으로 제한- 프로덕션에서는
telnetd대신dropbear(SSH) 사용 권장
고급 활용
mdev 경량 디바이스 관리자
mdev는 udev의 경량 대안으로, 임베디드 환경에서 /dev 디바이스 노드를 동적으로 관리합니다.
mdev의 설정 문법은 공식 mdev 문서에서 상세히 설명합니다:
| 항목 | mdev | udev (systemd) |
|---|---|---|
| 크기 | ~15 KB (BusyBox 내장) | ~2 MB (libsystemd 포함) |
| 설정 | /etc/mdev.conf | /etc/udev/rules.d/ |
| 규칙 문법 | 단순 정규식 + 소유권/권한 | 복잡한 키-값 매칭 |
| 핫플러그 | /proc/sys/kernel/hotplug 경유 | netlink 소켓 직접 수신 |
| 펌웨어 로드 | 지원 (CONFIG_FEATURE_MDEV_LOAD_FIRMWARE) | 기본 지원 |
# mdev 설정 및 실행
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s # 기존 디바이스 스캔하여 /dev 채우기
# /etc/mdev.conf 예시
# 형식: <regex> <uid>:<gid> <permissions> [=path] [@|$|*cmd]
console 0:0 0600
null 0:0 0666
zero 0:0 0666
random 0:0 0444
urandom 0:0 0444
tty[0-9] 0:5 0660
sd[a-z] 0:6 0660 @/etc/mdev/storage.sh # 삽입 시 스크립트 실행
sd[a-z] 0:6 0660 $/etc/mdev/storage.sh # 제거 시 스크립트 실행
httpd 경량 웹 서버
BusyBox httpd는 임베디드 장비의 관리 웹 인터페이스나 펌웨어 업데이트 서버로 사용됩니다. 전체 크기가 약 20KB이면서 CGI 스크립팅, 기본 인증(Basic Auth), IP 기반 접근 제어를 지원합니다:
| 기능 | 지원 여부 | 비고 |
|---|---|---|
| 정적 파일 서빙 | 지원 | MIME 타입 자동/수동 매핑 |
| CGI (Common Gateway Interface) | 지원 | /cgi-bin/ 경로 기본 |
| 기본 인증 (Basic Auth) | 지원 | httpd.conf에서 경로별 설정 |
| IP 접근 제어 | 지원 | A:(허용)/D:(차단) 규칙 |
| SSL/TLS | 미지원 | stunnel 또는 리버스 프록시 필요 |
| 디렉토리 리스팅 | 지원 | D:*로 비활성화 가능 |
| 가상 호스트 | 미지원 | 단일 호스트만 지원 |
| Keep-Alive | 미지원 | 연결당 하나의 요청 |
# 기본 실행 (포트 8080, /var/www 루트)
httpd -p 8080 -h /var/www -f # -f: 포그라운드 실행
# CGI 스크립트 예시 (/var/www/cgi-bin/status.sh)
#!/bin/sh
echo "Content-type: text/html"
echo ""
echo "<h1>System Status</h1>"
echo "<pre>"
echo "=== Uptime ===" && uptime
echo "=== Memory ===" && free
echo "=== Disk ===" && df -h
echo "=== Network ===" && ifconfig eth0
echo "</pre>"
# httpd.conf 설정 (/var/www/httpd.conf)
# 디렉토리별 인증 (사용자:비밀번호)
/cgi-bin:admin:s3cretPass
/admin:admin:s3cretPass
# IP 접근 제어 (A=허용, D=차단)
A:192.168.1.0/24 # 로컬 네트워크 허용
D:* # 나머지 차단
# MIME 타입 추가
.json:application/json
.svg:image/svg+xml
# 펌웨어 업데이트 CGI 예시 (/var/www/cgi-bin/upload.sh)
#!/bin/sh
echo "Content-type: text/plain"
echo ""
# POST 데이터를 파일로 저장
cat > /tmp/firmware.bin
if [ -s /tmp/firmware.bin ]; then
echo "Firmware received, flashing..."
mtd write /tmp/firmware.bin firmware
reboot
else
echo "Error: No firmware data received"
fi
udhcpc/udhcpd DHCP
udhcpc는 BusyBox의 DHCP 클라이언트로, 스크립트 기반으로 네트워크를 설정합니다:
# DHCP 클라이언트 실행
udhcpc -i eth0 -s /usr/share/udhcpc/default.script
# default.script 예시 (/usr/share/udhcpc/default.script)
#!/bin/sh
case "$1" in
bound|renew)
ifconfig "$interface" "$ip" netmask "$subnet"
if [ -n "$router" ]; then
route del default 2>/dev/null
route add default gw "$router"
fi
echo "nameserver $dns" > /etc/resolv.conf
;;
deconfig)
ifconfig "$interface" 0.0.0.0
;;
esac
ash 셸 스크립팅 주의사항
BusyBox ash에서 스크립트를 작성할 때 bash 비호환 사항에 주의해야 합니다. ash는 POSIX Shell Command Language를 기반으로 하며, ShellCheck 도구를 사용하면 스크립트의 이식성 문제를 사전에 검출할 수 있습니다:
# ✗ bash 전용 — ash에서 오류 발생
array=(one two three) # 배열 미지원
[[ $x == "test" ]] # [[ ]] 미지원
echo ${var^^} # 대소문자 변환 미지원
diff <(ls dir1) <(ls dir2) # 프로세스 치환 미지원
# ✓ ash 호환 대안
set -- one two three; echo "$2" # 위치 매개변수 사용
[ "$x" = "test" ] # 단일 [ ] 사용
echo "$var" | tr 'a-z' 'A-Z' # 외부 명령 사용
ls dir1 > /tmp/a; ls dir2 > /tmp/b; diff /tmp/a /tmp/b
# 셸 스크립트 이식성 확인 팁
# 1. 스크립트 첫 줄: #!/bin/sh (#!/bin/bash 아님)
# 2. ShellCheck 도구로 POSIX 호환성 검사
# 3. FEATURE_SH_STANDALONE 활성화 시 PATH에 없는 외부 명령 동작 주의
실전 init.d 서비스 스크립트 패턴:
BusyBox 임베디드 시스템의 init.d 서비스 스크립트는 다음 패턴을 따릅니다. bash 의존성 없이 순수 POSIX sh로 작성해야 합니다:
#!/bin/sh
# /etc/init.d/S20network — 네트워크 초기화 스크립트 예시
DAEMON="/sbin/udhcpc"
IFACE="eth0"
PIDFILE="/var/run/udhcpc.${IFACE}.pid"
SCRIPT="/usr/share/udhcpc/default.script"
start() {
printf "Starting network on %s: " "$IFACE"
# 인터페이스 활성화
ifconfig "$IFACE" up
# DHCP 클라이언트 시작 (백그라운드, PID 파일 생성)
start-stop-daemon -S -b -m -p "$PIDFILE" \
-x "$DAEMON" -- -i "$IFACE" -s "$SCRIPT" -p "$PIDFILE"
if [ $? -eq 0 ]; then
echo "OK"
else
echo "FAIL"
return 1
fi
}
stop() {
printf "Stopping network on %s: " "$IFACE"
start-stop-daemon -K -p "$PIDFILE" -x "$DAEMON"
ifconfig "$IFACE" down
rm -f "$PIDFILE"
echo "OK"
}
case "$1" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
*) echo "Usage: $0 {start|stop|restart}"; exit 1 ;;
esac
ash에서의 고급 패턴:
# 설정 파일 파싱 (연관 배열 대신 eval 사용)
while IFS='=' read -r key value; do
case "$key" in
\#*|"") continue ;; # 주석, 빈 줄 무시
esac
eval "cfg_${key}='${value}'"
done < /etc/myapp.conf
echo "Server: $cfg_server, Port: $cfg_port"
# 함수에서 값 반환 (nameref 미지원이므로 서브셸 stdout 사용)
get_ip() {
ifconfig "$1" 2>/dev/null | grep 'inet addr' | \
sed 's/.*addr:\([0-9.]*\).*/\1/'
}
MY_IP=$(get_ip eth0)
# 간단한 로깅 함수
log() {
local level="$1"; shift
logger -t "myapp" -p "daemon.${level}" "$*"
echo "[$(date '+%H:%M:%S')] ${level}: $*"
}
log info "Service started"
log err "Connection failed"
트러블슈팅
자주 발생하는 문제
| 증상 | 원인 | 해결 |
|---|---|---|
applet not found | 해당 애플릿이 빌드에 포함되지 않음 | make menuconfig에서 애플릿 활성화 후 재빌드 |
/bin/sh: can't execute | 동적 링크 빌드인데 공유 라이브러리 누락 | CONFIG_STATIC=y로 정적 빌드 또는 ldd busybox로 의존성 확인 |
initramfs 부팅 시 not syncing: No init found | /init 스크립트 없거나 실행 권한 없음 | chmod +x init, #!/bin/sh 첫 줄 확인 |
| 크로스 컴파일 빌드 실패 | CROSS_COMPILE 접두사 오류 | which ${CROSS_COMPILE}gcc로 경로 확인 |
mdev -s 실행 시 디바이스 누락 | /sys 마운트 안 됨 | mount -t sysfs none /sys 먼저 실행 |
getty 반복 재시작 | 터미널 장치 미존재 또는 권한 문제 | /dev/ttyS0 존재 확인, console=ttyS0 커널 파라미터 확인 |
| 정적 빌드 시 링크 에러 (glibc) | glibc 정적 빌드 라이브러리 누락 | apt install libc6-dev 또는 musl 사용 |
ash: syntax error 스크립트 오류 | bash 전용 문법 사용 | 위 ash 스크립팅 주의사항 참고, POSIX 호환 문법으로 수정 |
디버깅 기법
BusyBox 자체와 BusyBox 기반 시스템을 디버깅하는 다양한 방법입니다:
1. 빌드 시점 진단
# 디버그 정보 포함 빌드
make menuconfig
# Settings → Build with debug information (CONFIG_DEBUG=y)
# Settings → Build with extra compiler warnings (CONFIG_WERROR=y)
make V=1 -j$(nproc) # V=1: 상세 빌드 출력
# 빌드에 포함된 애플릿 확인
./busybox --list # 활성화된 모든 애플릿
./busybox --list-full # 설치 경로 포함 목록
# 바이너리 정보 확인
file busybox # ELF 타입, 아키텍처, 링크 방식
size busybox # text/data/bss 세그먼트 크기
nm busybox | grep _main # 포함된 xxx_main 심볼 확인
readelf -d busybox # 동적 링크 의존성 (정적이면 빈 출력)
2. 런타임 디버깅
# strace로 시스템 콜 추적
strace -f busybox ls /tmp # 단일 애플릿 추적
strace -f -e trace=open,read,write busybox cat /etc/hosts # 특정 syscall만
# ltrace로 라이브러리 함수 추적 (동적 링크 빌드만)
ltrace busybox ls /tmp
# BusyBox 자체 디버그 출력
# CONFIG_FEATURE_VERBOSE_USAGE=y — 상세 도움말
busybox ls --help # 상세 옵션 설명
3. QEMU + GDB 원격 디버깅
# 터미널 1: QEMU 실행 (GDB 서버 활성화)
qemu-system-x86_64 -kernel bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0 nokaslr" \
-nographic -s -S # -s: GDB 서버 :1234, -S: CPU 정지
# 터미널 2: GDB 연결
gdb busybox_unstripped # strip 전 바이너리 사용
(gdb) target remote :1234
(gdb) break ls_main # 특정 애플릿 브레이크포인트
(gdb) break find_applet_by_name # 디스패치 과정 추적
(gdb) continue
(gdb) bt # 백트레이스로 호출 경로 확인
4. initramfs 부팅 문제 진단
# initramfs 내용물 검증
mkdir /tmp/check && cd /tmp/check
zcat initramfs.cpio.gz | cpio -idmv # 풀어서 확인
ls -la init # 실행 권한 확인
head -1 init # #!/bin/sh 확인
file bin/busybox # 아키텍처 확인
bin/busybox --list | wc -l # 애플릿 수 확인
# 커널 부팅 파라미터로 디버깅
# rdinit=/bin/sh — init 대신 직접 셸 실행
# init=/bin/sh — initramfs의 /init 대신 /bin/sh
qemu-system-x86_64 -kernel bzImage \
-initrd initramfs.cpio.gz \
-append "console=ttyS0 rdinit=/bin/sh" -nographic
참고 자료
공식 자료
| 자료 | URL | 설명 |
|---|---|---|
| 공식 사이트 | busybox.net | 다운로드, 뉴스, 릴리스 정보 |
| FAQ | busybox.net/FAQ.html | 자주 묻는 질문과 답변 |
| 소스 저장소 (Git) | git.busybox.net/busybox | 공식 Git 저장소, 커밋 로그, 브랜치 |
| 소스 미러 (GitHub) | github.com/mirror/busybox | GitHub 읽기 전용 미러 — 코드 탐색에 편리 |
| 다운로드 | busybox.net/downloads | 안정 릴리스 tarball, 패치, 스냅샷 |
| 버그 트래커 | bugs.busybox.net | Bugzilla 기반 버그 리포트, 기능 요청 |
| 메일링 리스트 | busybox ML | 개발 토론, 패치 리뷰, 릴리스 공지 |
| 애플릿 목록 | BusyBox.html | 전체 애플릿 매뉴얼 (명령어별 옵션 설명) |
| 라이선스 | busybox.net/license.html | GPLv2 라이선스 전문 및 라이선스 준수 안내 |
| BusyBox 소개 | busybox.net/about.html | BusyBox 프로젝트 개요, 지원 아키텍처, 설계 철학 |
| 링크 목록 (심볼릭 링크) | BusyBox 명령어 레퍼런스 (busybox.net) | 각 애플릿의 사용법, 옵션, GNU 대비 차이점 |
관련 프로젝트
| 프로젝트 | URL | 관계 |
|---|---|---|
| toybox | Toybox — BSD 라이선스 BusyBox 대안 (landley.net) | BSD 라이선스 대안. Android에서 BusyBox 대체 |
| toybox vs BusyBox | Toybox FAQ — BusyBox와의 차이점 (landley.net) | Toybox 설계 철학, BusyBox 대비 장단점 비교 |
| Alpine Linux | Alpine Linux — BusyBox 기반 배포판 (alpinelinux.org) | BusyBox + musl 기반 경량 리눅스 배포판 |
| Alpine Wiki | Alpine Wiki — BusyBox 설정 가이드 (wiki.alpinelinux.org) | Alpine에서의 BusyBox 구성, 대체 패키지 설치 방법 |
| musl libc | musl libc — 정적 링킹에 최적화된 C 라이브러리 (musl.libc.org) | BusyBox 정적 빌드에 자주 사용되는 경량 C 라이브러리 |
| musl FAQ | musl과 glibc 기능 차이 (wiki.musl-libc.org) | musl 정적 빌드 시 주의할 glibc 호환성 차이 |
| Buildroot | Buildroot — 임베디드 빌드 시스템 (buildroot.org) | BusyBox 기본 채택 임베디드 빌드 시스템 |
| Buildroot BusyBox 설정 | Buildroot 매뉴얼 — BusyBox 커스터마이징 (buildroot.org) | Buildroot에서 BusyBox 설정 파일 커스터마이징 방법 |
| OpenWrt | OpenWrt — BusyBox 기반 라우터 펌웨어 (openwrt.org) | BusyBox 기반 라우터 펌웨어 |
| uClibc-ng | uClibc-ng — 임베디드 C 라이브러리 (uclibc-ng.org) | 임베디드용 경량 C 라이브러리 (BusyBox와 자주 조합) |
| dropbear | Dropbear — 경량 SSH 서버 (matt.ucc.asn.au) | BusyBox 환경에서 자주 사용되는 경량 SSH 서버 |
| sbase/ubase | sbase — 최소주의 유닉스 유틸리티 (suckless.org) | suckless 프로젝트의 최소주의 유닉스 유틸리티 |
| runit | runit — 경량 init/서비스 관리자 (smarden.org) | BusyBox init 대안으로 사용되는 경량 서비스 감독 시스템 |
| GNU Coreutils | GNU Coreutils — BusyBox 비교 대상 (gnu.org) | BusyBox가 경량 대체하는 GNU 핵심 유틸리티 전체 목록 |
| Yocto Project | Yocto Project — 임베디드 리눅스 빌드 (yoctoproject.org) | BusyBox를 rootfs에 포함하는 임베디드 리눅스 빌드 프레임워크 |
커널 관련 문서
| 문서 | URL | 설명 |
|---|---|---|
| initramfs 가이드 | ramfs/rootfs/initramfs 공식 문서 (kernel.org) | 커널 공식 initramfs/initrd 문서 |
| early userspace | 초기 사용자 공간 지원 (kernel.org) | 초기 사용자 공간 지원 (gen_init_cpio, CONFIG_INITRAMFS_SOURCE) |
| devtmpfs | devtmpfs 커널 문서 (kernel.org) | mdev 대안인 커널 기반 /dev 자동 생성 |
| Kconfig 언어 | Kconfig 언어 명세 (kernel.org) | BusyBox가 사용하는 동일한 Kconfig 시스템 문서 |
| Kbuild 시스템 | Kbuild 빌드 시스템 (kernel.org) | BusyBox Makefile 구조가 차용한 커널 빌드 시스템 |
| switch_root | initramfs에서 실제 rootfs 전환 (kernel.org) | BusyBox switch_root 명령이 수행하는 루트 전환 메커니즘 |
학습 자료
| 자료 | URL/출처 | 설명 |
|---|---|---|
| BusyBox 소스 해부 | BusyBox 아키텍처 분석 (IBM DeveloperWorks) | M. Tim Jones의 BusyBox 아키텍처 분석 기사 |
| Embedded Linux Primer | Christopher Hallinan 저 (Prentice Hall) | BusyBox 챕터 포함 임베디드 리눅스 입문서 |
| Mastering Embedded Linux Programming | Frank Vasquez, Chris Simmonds 저 (Packt) | BusyBox init, rootfs 구축 실습 포함 |
| Linux From Scratch (LFS) | Linux From Scratch — 리눅스 직접 구축 (linuxfromscratch.org) | 리눅스 시스템 직접 구축 — BusyBox 대신 GNU 유틸리티 사용하지만 구조 이해에 도움 |
| POSIX Shell Reference | POSIX 셸 명세 (pubs.opengroup.org) | ash 스크립팅의 기준이 되는 POSIX 셸 명세 |
| POSIX Utilities | POSIX 유틸리티 목록 (pubs.opengroup.org) | BusyBox 애플릿이 구현하는 POSIX 표준 유틸리티 전체 목록 |
| ShellCheck | ShellCheck — 셸 스크립트 정적 분석 (shellcheck.net) | 셸 스크립트 정적 분석 도구 — bash/POSIX 호환성 검사 |
| Minimal Linux Live | Minimal Linux Live — BusyBox 기반 최소 리눅스 (github.com) | 커널 + BusyBox만으로 부팅하는 최소 리눅스 교육 프로젝트 |
| Arch Wiki — BusyBox | BusyBox 설정 및 활용 (wiki.archlinux.org) | BusyBox 설치, initramfs 구성, mkinitcpio 통합 가이드 |
| Gentoo Wiki — BusyBox | BusyBox 구성 가이드 (wiki.gentoo.org) | Gentoo에서의 BusyBox 구성, initramfs 생성, 복구 셸 활용 |
관련 문서
| 문서 | 관련 내용 |
|---|---|
| 부팅 과정 | 커널 부팅에서 init 실행까지의 전체 흐름 |
| 개발 환경 설정 | BusyBox 기반 initramfs 생성 및 커널 개발 환경 |
| QEMU 에뮬레이션 | BusyBox initramfs를 QEMU에서 테스트하는 방법 |
| 임베디드 빌드 시스템 | Buildroot/OpenWrt/Yocto에서 BusyBox 통합 |
| RAM 디스크 | initramfs와 initrd의 차이, 루트 파일시스템 구성 |
| Linux From Scratch | 크로스 컴파일 환경 구축 |
| 커널 디버깅 | QEMU+GDB 디버깅 환경 설정 |
| GDB 디버깅 | GDB를 이용한 BusyBox/커널 디버깅 |