카드뉴스










빌드는 C 파일을 바로 MCU 코드로 바꾸는 한 번의 작업이 아닙니다
IDE에서 Build 버튼을 누르면 소스 코드가 곧바로 MCU에서 실행되는 파일이 되는 것처럼 보입니다. 실제로는 전처리, 컴파일, 어셈블, 링크, 이미지 변환 같은 여러 단계를 거칩니다. 이 흐름을 알아야 include 오류, undefined reference, section overflow, flash 다운로드 문제를 서로 구분해서 볼 수 있습니다.
전처리 단계에서는 `#include`, `#define`, 조건부 컴파일이 먼저 처리됩니다. 헤더 파일이 펼쳐지고 매크로가 치환되기 때문에, 컴파일러가 실제로 보는 코드는 사용자가 편집한 C 파일과 조금 다를 수 있습니다. 같은 코드가 어떤 설정에서는 빌드되고 어떤 설정에서는 빠지는 이유도 `#ifdef`와 빌드 옵션을 보면 드러나는 경우가 많습니다.
임베디드 프로젝트에서는 전처리 옵션이 보드 설정과도 연결됩니다. MCU 모델, 클럭 소스, HAL 드라이버 사용 여부, RTOS 사용 여부가 매크로 하나로 갈리는 경우가 많습니다. 따라서 빌드가 이상할 때는 소스 파일만 보지 말고 include path, preprocessor symbols, compiler flags를 같이 확인해야 합니다. 같은 파일을 열어도 Debug와 Release 설정에서 실제로 컴파일되는 코드가 달라질 수 있습니다.
컴파일과 어셈블은 object file을 만듭니다
컴파일 단계에서는 C 또는 C++ 코드가 어셈블리 수준의 명령으로 바뀝니다. 타입 오류, 문법 오류, 함수 선언 불일치 같은 문제는 보통 이 단계에서 나타납니다. 이후 어셈블 과정을 거치면 각 소스 파일마다 object file이 만들어집니다. 이 object file은 아직 전체 프로그램이 아니라, 나중에 합쳐질 조각에 가깝습니다.
그래서 어떤 함수가 다른 파일에 있어도 컴파일은 통과할 수 있습니다. 컴파일러는 “그런 함수가 있을 것”이라는 선언만 믿고 넘어갈 수 있기 때문입니다. 하지만 실제 정의가 없거나 라이브러리가 연결되지 않으면 링크 단계에서 undefined reference 오류가 납니다. 반대로 선언이 없으면 컴파일 단계에서 먼저 경고나 오류가 나올 수 있습니다.
링크 단계에서 메모리 배치가 결정됩니다
링커는 여러 object file과 라이브러리를 묶어 하나의 실행 이미지로 만듭니다. 임베디드에서는 링커 스크립트가 특히 중요합니다. 코드와 상수는 flash에, 전역 변수와 stack, heap은 RAM에 배치되는 식으로 메모리 영역이 정해지기 때문입니다. 코드가 커져 flash가 넘치거나 버퍼가 늘어나 RAM이 부족해지는 문제도 이 단계에서 드러납니다.
링크 결과물로 ELF 파일이 만들어지면 디버그 심볼, 섹션 정보, 주소 정보가 함께 들어 있을 수 있습니다. 실제 플래시에 쓰기 위해 HEX나 BIN 파일로 변환하는 단계가 뒤따르기도 합니다. 디버거가 함수 이름을 보여줄 수 있는 이유와, 다운로드 도구가 특정 주소에 이미지를 쓰는 이유가 이 결과물 안에 들어 있습니다.
빌드 로그는 오류 위치를 나눠 보는 지도입니다
빌드 오류를 볼 때는 메시지의 단계부터 확인하면 좋습니다. 헤더를 못 찾으면 include path나 전처리 조건을 보고, 문법 오류면 해당 소스 파일을 봅니다. undefined reference는 링크 대상과 함수 정의를 확인하고, region overflow는 링커 스크립트와 메모리 사용량을 봐야 합니다. 빌드 과정을 단계별로 나누면 오류 메시지가 훨씬 덜 막연해집니다.
또 하나 자주 놓치는 부분은 빌드 결과물과 다운로드 대상의 차이입니다. ELF는 디버깅 정보까지 담을 수 있지만, 실제 양산이나 부트로더 업데이트에는 BIN 또는 HEX가 쓰일 수 있습니다. map 파일을 보면 어떤 함수와 전역 변수가 flash와 RAM을 얼마나 쓰는지 확인할 수 있습니다. 빌드 버튼 뒤의 흐름을 알면 오류 해결뿐 아니라 코드 크기 최적화와 메모리 사용량 분석도 훨씬 구체적으로 할 수 있습니다.