소스에서 실행 파일까지
110분 안팎
학습 목표
gcc로 전처리·컴파일·링크를 나누고 누락된 함수 정의를 복구합니다.
개념
서로 다른 소스의 정의를 이어 실행합니다
기록기의 판정 함수를 만들었으니 이제 입력을 읽는 main.c와 판정하는 sensor.c를 따로 컴파일합니다. 함수가 다른 파일에 있다고 실행 파일이 자동으로 그 파일을 찾는 것은 아닙니다. 링크 명령에 정의가 들어 있는 객체를 포함해야 합니다. 이번 레슨의 목표는 파일을 더 나누는 일보다 소스, 선언, 객체, 실행 파일의 관계를 설명하고 정의를 빠뜨린 빌드를 재현해 고치는 것입니다.
PC 센서 기록기 실습은 이 모듈의 미션 시작점이기도 합니다. 앞 모듈은 없으므로 빈 프로젝트에서 제공한 파일로 출발합니다. 완성한 sensor.h, sensor.c, main.c, Makefile, 테스트는 다음 모듈의 고정 샘플 버퍼 확장에 넘깁니다. 출력 형식과 입력 계약을 README.md에 남겨 다음 작성자가 기존 동작을 회귀 검사할 수 있게 합니다. 지금은 표준 입력이 하드웨어 경계이며 GPIO나 타이머를 구현했다고 표현하지 않습니다.
전처리, 객체 생성, 링크를 분리합니다
gcc -E main.c는 전처리까지만 수행해 헤더 내용이 포함된 C 텍스트를 냅니다. 출력이 길어서 main.i 파일로 저장하고 sample_valid 선언이 펼쳐져 있는지 확인합니다. main.i는 실행 파일이 아니며 시스템 헤더까지 들어 있어 사람의 원본 소스보다 훨씬 길 수 있습니다. #include는 함수 정의를 원격에서 가져오는 동작이 아닙니다. sensor.h의 선언은 호출 모양을 알게 하지만 sensor.c의 본체를 넣어 주지는 않습니다.
gcc -c main.c -o build/main.o는 필요한 전처리와 컴파일·어셈블을 수행하여 객체 파일을 만듭니다. 같은 방식으로 sensor.c는 build/sensor.o가 됩니다. 여기서 -c가 최종 실행 파일 링크를 멈추는 옵션입니다. main.o에는 sample_valid를 사용할 참조가 있고 sensor.o에는 그 함수의 정의가 있습니다. 두 파일은 따로 번역되므로 main.c의 컴파일 성공은 전체 실행 파일 생성 성공을 보장하지 않습니다.
gcc build/main.o build/sensor.o -o recorder는 두 객체와 필요한 기본 런타임을 연결합니다. 표준 입출력 라이브러리의 연결은 컴파일러 드라이버가 처리하므로 이번 실습에서는 링커를 직접 호출하지 않습니다. 링크가 끝나면 ./recorder로 프로그램을 실행할 수 있습니다. ./는 현재 디렉터리를 가리킵니다. 파일 이름만 recorder라고 입력하면 셸의 실행 파일 검색 경로에 없어서 못 찾을 수 있습니다.
일부러 sensor.o를 빼고 gcc build/main.o -o build/missing을 실행하면 sample_valid 정의를 찾지 못해 실패합니다. macOS에서는 Undefined symbols와 _sample_valid가, 다른 링커에서는 undefined reference와 sample_valid가 보일 수 있습니다. 먼저 함수 이름과 링크 명령에 나열된 객체를 대조합니다. 선언을 이미 알고 main.o가 생긴 상태라면 헤더를 반복해서 포함하는 것으로 정의 누락은 해결되지 않습니다.
함수 선언 자체가 없으면 상황이 다릅니다. main.c가 sample_valid의 호출 형태를 모르는 컴파일 진단을 냅니다. 이 경우에는 sensor.h의 선언과 #include를 확인합니다. 반면 선언은 있는데 정의 이름이 다르거나 해당 객체가 명령에 없다면 링크 단계에서 실패합니다. 오류의 마지막 한 줄만 보지 않고 먼저 실패한 명령이 -c였는지 최종 객체 연결이었는지 찾습니다. 단계가 고칠 위치를 좁혀 줍니다.
Makefile에 재현 가능한 연결을 적습니다
Makefile의 대상: 의존 파일 목록 아래에 실행 명령이 옵니다. 명령 첫 글자는 탭입니다. build/main.o는 main.c와 sensor.h를 의존하고 build/sensor.o는 sensor.c와 sensor.h를 의존합니다. 헤더가 바뀌면 양쪽 객체를 다시 만들도록 관계를 적습니다. recorder는 두 객체가 필요합니다. $@는 현재 대상 이름이므로 객체 규칙에서는 객체 경로, 실행 파일 규칙에서는 recorder를 가리킵니다.
CC는 gcc, CFLAGS는 C11과 경고 옵션 및 디버깅 옵션으로 지정합니다. 빌드 목록과 링크 명령을 따로 봅니다. recorder의 의존 목록에 sensor.o가 있어도 실행 명령에 main.o만 넣으면 정의를 포함하지 못합니다. 이번 starter에는 이 실수가 들어 있습니다. 의존성은 무엇을 먼저 만들지 결정하며 링크 명령 인자는 무엇을 실제로 연결할지 결정합니다. 두 부분이 일치해야 합니다.
make test는 check.py를 통해 함수를 검사하고 객체 생성, 정의 누락 링크의 예상 실패, 정상 링크, CLI 출력까지 확인합니다. 정의 누락 명령의 실패는 의도한 검사이므로 그 자체를 최종 실패로 세지 않습니다. check.py는 종료 코드가 0이 아닌지와 진단에 함수명이 있는지를 검사한 다음 정상 링크가 성공해야 통과시킵니다. 아무 컴파일 오류든 나면 통과하는 테스트가 되지 않도록 객체 생성은 먼저 성공해야 합니다.
미션 starter에는 범위 함수의 하한 누락과 recorder 링크의 객체 누락이 함께 있습니다. 첫 실행은 범위 검사에서 멈춥니다. 먼저 음수 수락을 고치고 다시 실행하면 링크 규칙의 누락이 드러납니다. 테스트가 뒤 실패를 아직 보여 주지 않았다고 뒤 동작까지 맞다고 판단하지 않습니다. make clean 후 모든 검사를 마치는 시점의 출력과 종료 코드를 최종 근거로 남깁니다.
CLI의 계약과 실습에서 제공한 경계를 봅니다
CLI는 공백으로 구분된 정수 토큰을 읽어 정상값이면 OK raw, 범위 밖 정수이면 INVALID를 냅니다. 정상값만 SUMMARY count min max에 포함하며 유효값이 없으면 SUMMARY 0 NONE NONE입니다. 반복된 값도 별도 표본입니다. 각 토큰은 63자 이하, 전체는 100만 토큰 이하를 계약으로 정합니다. 이런 제한 밖 입력을 지원한다고 주장하지 않습니다. 이번 미션은 파일 분리와 범위 검사에 집중합니다.
main.c의 입력 보호 코드는 제공되어 있습니다. strtol로 십진 문자열을 long으로 읽고 변환 끝에 글자가 남았는지, 변환 범위 오류가 있는지, int 범위를 넘는지 확인합니다. 12x는 숫자 접두부만 읽었다고 수락하지 않습니다. 형식이 틀리면 stderr에 ERROR token을 내고 종료 코드 2로 멈춥니다. 전에 출력한 표본은 남지만 요약은 내지 않습니다. -1은 정수 형식이 맞아서 INVALID 뒤에도 다음 표본을 계속 읽습니다.
표준 출력은 채점할 표본과 요약, 표준 오류는 입력 실패 진단에 사용합니다. 정상 실행은 종료 코드 0이고 빈 입력도 정상입니다. 터미널에서 입력을 직접 주면 입력 종료까지 기다릴 수 있어, 따라하기는 printf로 데이터를 보내 끝을 명확하게 만듭니다. 앞 명령의 종료 코드는 다른 명령을 실행하기 전에 status에 담습니다. 이후 printf가 성공했는지를 읽으면 기록기의 실패를 놓칠 수 있습니다.
README에는 사용한 컴파일러 이름과 버전, 운영체제·CPU, 전처리·객체 생성·링크 명령, 실제 누락 오류의 함수명, 복구 후 검사 결과를 적습니다. 이 작성 환경은 Apple Silicon의 Apple Clang을 gcc 이름으로 호출했으며 학습자는 자기 환경으로 바꾸어 기록합니다. 예제 출력의 아키텍처 이름을 복사해 자기 측정이라고 적지 않습니다. 오류 문구가 달라도 정의가 없던 원인과 추가한 객체를 설명할 수 있어야 합니다.
완성 뒤에는 make test가 함수 7개와 CLI 10개를 확인하는지 살펴보고, README에서 PC 검증과 실물 미검증을 나눕니다. 센서 전압, 버스 타이밍, 전송 중 분리는 아직 시험하지 않았습니다. 다음 모듈에서는 이 CLI의 정수 raw를 시간과 함께 버퍼에 담도록 확장합니다. 그러므로 다음 작성자에게 기능이 동작하는 소스와 동일한 검사를 함께 넘겨야 합니다. 실행 파일 하나만 넘기면 수정과 재현이 어려워집니다.
따라하기
전처리에서 선언 확인
PC 기록기 starter에서 이전 레슨처럼 sample_valid의 하한을 먼저 복구합니다. 다음 명령은 main.i에서 함수 선언이 포함됐는지 확인합니다.
gcc -std=c11 -E main.c -o main.i
python3 -c 'from pathlib import Path; print("declaration present:", "int sample_valid(int raw);" in Path("main.i").read_text())'실행 결과
declaration present: True
파일을 각각 객체로 만들기
객체 컴파일은 -c로 링크를 하지 않습니다. 만들어진 파일 이름을 확인합니다.
mkdir -p build
gcc -std=c11 -Wall -Wextra -Werror -c main.c -o build/main.o
gcc -std=c11 -Wall -Wextra -Werror -c sensor.c -o build/sensor.o
ls build/main.o build/sensor.o실행 결과
build/main.o build/sensor.o
정의 누락 재현
sensor.o 없이 연결합니다. 실제 오류 전체는 link.err에 저장합니다. cat link.err로 함수명과 링크 오류를 읽습니다. 아래 출력은 원문 길이와 관계없이 종료 코드와 해당 함수명이 들어 있는지를 확인한 결과입니다.
gcc build/main.o -o build/missing 2>link.err
status=$?
printf "link exit=%s\n" "$status"
python3 -c 'from pathlib import Path; print("sample_valid named:", "sample_valid" in Path("link.err").read_text())'실행 결과
link exit=1 sample_valid named: True
정의를 포함해 실행
starter Makefile의 recorder 명령에 build/sensor.o를 추가합니다. 여기서는 같은 연결을 직접 실행하고 0, 상한, 상한 밖의 결과를 관찰합니다.
gcc build/main.o build/sensor.o -o recorder
printf "0 4095 4096\n" | ./recorder실행 결과
OK 0 OK 4095 INVALID SUMMARY 2 0 4095
전체 회귀 검사
Makefile과 함수 수정 후 산출물을 지우고 전체 검사를 실행합니다. -s는 make 명령 에코를 줄이는 옵션이며 일반 make test도 같은 검사를 수행합니다.
make -s clean
make -s test실행 결과
range: 7 cases, 0 failures link: missing definition rejected cli: 10 cases passed PASS pc-workbench
확인 문제
실습
PC 기록기 starter에서 sensor.c의 하한 누락을 복구하고 Makefile의 recorder 링크 명령에 정의가 담긴 객체를 포함합니다. main.c와 테스트는 제공된 계약을 유지합니다. make test로 함수, 의도한 링크 실패, 복구 링크, CLI를 모두 검사합니다. README.md의 환경·명령·실제 오류·복구 결과·PC 검증 한계를 자기 관찰로 채웁니다. 이 결과가 이번 미션이자 다음 모듈 starter의 출발점입니다.
실행 명령
make test
기대 결과
range: 7 cases, 0 failures / link: missing definition rejected / cli: 10 cases passed / PASS pc-workbench / 종료 코드 0
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 컴파일은 성공했지만 링크가 실패한 상황을 설명해 주시면 됩니다.