Troubleshooting

[C언어 빌드] extern 전역 변수 중복 정의(Multiple Definition) 및 참조 에러 해결

임베디드 친구 2026. 6. 13. 20:25
반응형

[Quick Summary For Global Developers]

  • Symptom: 컴파일은 성공하지만, 링크 단계에서 "linker returned 1 exit status" 문구와 함께 "multiple definition of [variable_name]" 에러가 발생하며 빌드가 중단됨.
  • Cause: 헤더 파일에 extern 키워드 없이 전역 변수를 선언 및 초기화한 후, 해당 헤더를 복수의 소스 파일(.c)에서 #include하여 참조함에 따라 각 목적 파일(.o)마다 독립된 전역 변수 메모리 공간이 중복 생성됨.
  • Solution: 헤더 파일에는 extern 한정자만을 사용하여 전역 변수의 '참조 규격(Declaration)'만 명시하고, 실제 메모리를 할당하는 '정의(Definition)' 및 초기화는 단 하나의 소스 파일(.c)에서 수행함.

링커 오류(Linker Error) 중복 정의(Multiple Definition) 발생 증상

멀티 소스 파일 구조를 갖는 C언어 프로젝트를 빌드할 때, 개별 소스 코드(.c) 단독 컴파일 단계에서는 문법 오류(Syntax Error)가 발견되지 않아 성공적인 로그가 출력됩니다. 그러나 생성된 오브젝트 파일들을 하나의 바이너리로 병합하는 링크(Linking) 단계에서 툴체인은 아래와 같은 에러 메시지를 콘솔에 출력하는 경우가 있습니다.

arm-none-eabi-ld: ./src/sensor.o:/workspace/include/global.h:12: multiple definition of `g_system_status'; ./src/main.o:/workspace/include/global.h:12: first defined here
collect2: error: ld returned 1 exit status

이 현상은 복수의 번역 단위(Translation Unit)가 동일한 식별자를 가진 전역 변수를 각자 메모리 공간에 정적으로 할당하려 할 때 링커가 주소 결합에 실패하면서 발생합니다.

전역 변수 선언(Declaration)과 정의(Definition) 분리 메커니즘의 근본적인 원인 분석

이 에러의 근본 원인은 C언어 컴파일러의 컴파일 단위 분리와 링커의 심볼 테이블(Symbol Table) 확인 간의 상호작용에 있습니다.
C언어에서 헤더 파일(.h)은 독립적으로 컴파일되지 않으며, 소스 파일(.c)에서 #include 지시자를 만나는 순간 해당 소스 파일 내부로 결합됩니다. 이때 헤더 파일에 int g_system_status = 0;과 같이 변수를 선언하고 초기화 값을 할당하면, 이 헤더를 포함하는 모든 소스 파일은 각자 컴파일을 거치며 독립적인 데이터 영역(Data Section)에 해당 심볼을 각각 생성합니다.
컴파일러 툴체인은 각 소스 파일 단위로 코드를 해석하여 기계어로 변환할 때 심볼 테이블에 외부로 노출할 심볼(Global Symbol) 정보들을 등록합니다.
main.c와 sensor.c에서 int g_system_status = 0; 변수를 선언한 동일한 헤더 파일을 include 한 경우.

  • main.c -> 컴파일 -> main.o (심볼 테이블에 g_system_status 등록)
  • sensor.c -> 컴파일 -> sensor.o (심볼 테이블에 g_system_status 등록)
    최종 링킹 프로세스에서 링커(Linker)는 오브젝트 파일들을 병합하며 중복된 전역 심볼을 학인합니다. 동일한 심볼에 대해 두 개 이상의 유효한 메모리 주소가 바인딩을 요청할 경우, 링커는 주소 충돌을 해결할 수 없으므로 multiple definition 예외를 발생시키며 링크 과정을 즉시 중단합니다.

중복 정의 링커 에러를 유발하는 잘못된 C 코드 예시 (Bad Case)

아래 코드는 다중 컴파일 환경에서 링커 에러를 유발하는 예시입니다. 헤더 파일 내부에 전역 변수의 메모리 공간 할당 코드가 포함되어 있습니다.

/*********************************
*
* global.h
*
**********************************/
#ifndef GLOBAL_H
#define GLOBAL_H

#include <stdint.h>

/* BAD: Variable definition inside header file. 
   Every file including this header allocates separate memory. */
uint32_t g_system_status = 0;

void status_update(void);

#endif /* GLOBAL_H */
/*********************************
*
* sensor.c
*
**********************************/
#include "global.h"

void status_update(void) {
    /* Accesses g_system_status defined in sensor.o */
    g_system_status = 1; 
}
/*********************************
*
* main.c
*
**********************************/
#include "global.h"

int main(void) {
    /* Conflict arises when linking main.o and sensor.o due to duplicate g_system_status */
    g_system_status = 0;
    status_update();
    while(1);
}

extern 키워드를 활용한 방어적 전역 변수 참조 C 코드 구현법 (Good Case)

문제를 해결하기 위해 헤더 파일에는 물리적 메모리를 할당하지 않는 extern 선언(Declaration)만 남겨두고, 실제 메모리 할당 및 초기화(Definition)는 단 하나의 지정된 소스 파일(.c) 내부로 격리합니다.

/*********************************
*
* global.h
*
**********************************/
#ifndef GLOBAL_H
#define GLOBAL_H

#include <stdint.h>

/* GOOD: Pure declaration using 'extern'. 
   Informs the compiler that this variable exists in some object file. */
extern uint32_t g_system_status;

void status_update(void);

#endif /* GLOBAL_H */
/*********************************
*
* global.c
*
**********************************/
#include "global.h"

/* GOOD: Actual definition and allocation of memory. 
   Executed only once across the entire project layout. */
uint32_t g_system_status = 0;
/*********************************
*
* sensor.c
*
**********************************/
#include "global.h"

void status_update(void) {
    /* Resolved correctly: refers to the single global symbol allocated in global.o */
    g_system_status = 1;
}
/*********************************
*
* main.c
*
**********************************/
#include "global.h"

int main(void) {
    /* References the shared symbol via external reference entry */
    g_system_status = 0;
    status_update();
    while(1);
}

핵심 수정 포인트 설명

  • extern 한정자의 롤바인딩: 헤더 파일에 명시된 extern uint32_t g_system_status; 문장은 해당 변수의 타입과 이름 규격만 컴파일러에 전달할 뿐, 바이너리 내의 실제 물리 RAM 영역을 점유하지 않습니다.
  • 단일 진입 정의 파일 매핑: 정적 주소 할당을 global.c에서 함으로써 링커는 g_system_status라는 심볼에 대해 유일한 메모리 주소(단 하나의 Target Symbol entry)만 할당 및 연동합니다.

빌드 파일(MAP) 분석 및 심볼 디버깅 팁 (Debugging Tips)

임베디드 프로젝트의 규모가 커짐에 따라 타사 라이브러리나 레거시 미들웨어가 얽히면서 복잡한 형태로 중복 정의가 발생할 수 있습니다. 이때 원인을 정밀 추적하는 실무 트러블슈팅 가이드입니다.

  • 맵 파일(Linker MAP File) 심볼 주소 검증: 빌드 옵션에서 맵 파일 출력을 활성화(-Wl,-Map=output.map)한 후 텍스트 에디터로 엽니다. 심볼 검색을 통해 에러가 발생한 변수명이 어떤 섹션(.data 또는 .bss)에 매핑되어 있는지 확인하고, 해당 심볼을 내보내는 소스 오브젝트의 할당 경로를 역추적합니다.
  • GNU NM 툴체인 유틸리티 활용: GCC 패키지에 내장된 툴체인 명령어 arm-none-eabi-nm을 사용하여 개별 오브젝트 파일의 심볼 속성을 직접 파악합니다.
arm-none-eabi-nm main.o | grep g_system_status

출력된 플래그 비트가 C (Common) 또는 D (Initialized Data)로 다중 출력된다면 해당 파일들이 각각 정의를 시도하고 있다는 뜻이므로, B (BSS) 나 D로 유일하게 정의된 파일 하나를 제외하고 모두 헤더 레벨에서 extern 처리를 거쳐야 함을 확인할 수 있습니다.

반응형