autoconf식 설정 탐지는 autoconf나 CMake 등이 대상 플랫폼에서 특정 기능을 사용할 수 있는지 확인하기 위해 테스트 프로그램을 컴파일하고 링크하는 방식이다. Boris Kolpackov는 이 방식의 낭비와 취약성, 속도, 변경 추적 문제를 짚고, 빌드 과정에 탐지를 통합하는 방법과 한계를 설명한다.

2026년 10월 8일 게시

autoconf와 CMake 등에서 사용하는 설정 탐지는 함수 같은 기능을 대상 플랫폼에서 사용할 수 있는지 알아보기 위해 테스트 프로그램을 컴파일하고 링크한다. 예를 들어 코드베이스에서 strl*() 계열 함수를 우선 사용하고 싶을 수 있다. 하지만 이 함수들은 아직 표준이 아니며 모든 libc 구현에 포함되어 있지도 않다. 따라서 함수가 있는지 확인하고, 없다면 대체 구현을 제공하거나 다른 방법을 사용할 수 있다. 한 가지 탐지 방법은 사용할 함수가 포함된 테스트 프로그램을 컴파일하고 링크하는 것이다. 성공하면 해당 함수를 사용할 수 있다고 판단한다.

언뜻 보면 이 접근은 매력적이다. 특히 아직 존재하지 않는 플랫폼을 지원하기 위해 별도 작업을 하지 않아도 되는 적응성이 있다. 예를 들어 누군가 Linux용 새 libc를 만들더라도 기존 strl*() 탐지가 알아서 처리하므로 별도 지원이 필요 없다. 이미 출시된 프로젝트 버전도 이 새 libc를 자동으로 지원하게 된다.

하지만 이 방식에는 몇 가지 성가신 문제가 있다.

  • 낭비가 발생한다. strl*() 함수가 오래전부터 제공된 FreeBSD에서도 매번 탐지를 컴파일할 필요는 없다. 극단적으로는 기능이 없는 최신 대상 플랫폼조차 탐지 자체를 실행할 수 없는데 계속 기능을 확인하는 상황이 된다. 관련 사례로 A Generation Lost in the Bazaar를 참고할 수 있다.
  • 취약하다. 테스트 프로그램의 컴파일이나 링크가 실패했다는 이유만으로 기능이 없다고 판단한다. 하지만 테스트 코드의 실수, 잘못된 빌드 설정, _GNU_SOURCE 같은 기능 테스트 매크로 누락 등 다른 이유로도 실패할 수 있다. 최근에는 부실하게 작성된 탐지가 거짓 음성을 내놓은 탓에 불만과 좌절이 이어졌다. GCC와 Clang이 오래전에 사용이 중단된 특정 C 구문을 더는 받아들이지 않으면서 탐지 코드의 컴파일이 실패한 사례다. 거짓 음성은 기능을 조용히 사용하지 않게 만들어 기능 누락이나 성능 저하로 이어질 수 있다.
  • 느리다. 탐지 하나를 컴파일하는 데 시간이 오래 걸리지는 않지만 수백 개를 컴파일하면 지연이 눈에 띈다. autoconf와 CMake는 이 작업을 직렬로 수행해 문제를 더 키운다.
  • 변경 사항을 추적하지 않는다. 기존 도구인 autoconf와 CMake는 입력이 바뀌어도 관련 탐지를 다시 실행하지 않는다. 예를 들어 strl*() 함수는 glibc 2.38에 추가됐다. glibc 2.37에서 업그레이드했다면 이미 설정을 마친 컴퓨터의 프로젝트들도 변경을 감지해 새 함수를 사용하기 시작해야 한다.

첫 번째 문제인 낭비를 해결하려면 접근 자체를 바꿔야 한다. 한 가지 대안은 ‘기대 기반 설정’이다. 특정 조건이 충족되면 기능을 사용할 수 있다고 가정하는 방식이다. 예를 들어 strl*() 함수는 FreeBSD를 대상으로 하거나 glibc 2.38 이상을 사용하면 제공된다고 가정할 수 있다. 물론 완전한 구현이라면 다른 플랫폼이나 libc 구현도 확인해야 한다. 예시는 HAVE_STRLCPY.h에 있다. 이 방식은 Qt와 FFmpeg처럼 설정이 복잡한 프로젝트에서 build2를 통해 성공적으로 사용됐으며, 자세한 내용은 libbuild2-autoconf에서 확인할 수 있다.

특수한 경우 등 어떤 이유로 설정 탐지를 계속해야 한다고 하자. 나머지 문제 중 취약성은 뒤에서 다루고, 우선 속도와 변경 추적 부족을 개선할 수 있을까?

설정 탐지의 작업을 간단히 말하면 테스트 프로그램 여러 개를 컴파일하고 링크하는 것이다. 필요한 결과는 프로그램 자체가 아니라 컴파일과 링크가 성공했는지 실패했는지다. 이 작업을 병렬로 수행하고 테스트 소스, 포함된 헤더의 재귀적 집합, 컴파일 및 링크 옵션 등 입력 변경도 추적하고 싶다.

이 문제의 형태는 익숙하다. 소스 파일 여러 개를 병렬로 컴파일하고 변경 사항을 제대로 추적해야 하는 작업은 기존 빌드 시스템이 이미 처리하고 있다. make도 이를 합리적으로 수행할 수 있다.

CMake는 각 탐지마다 개별 프로젝트를 생성한 뒤 그 프로젝트를 빌드할 기반 빌드 시스템을 실행하는 것으로 보인다. 하지만 탐지를 병렬로 실행하거나 변경 사항을 추적하는 데 이 구조를 활용하지는 않는다.

그렇다면 빌드 시스템으로 탐지를 수행하면 어떨까? 더 나아가 별도의 설정 및 프로젝트 생성 단계를 없앨 수는 없을까?

프로젝트를 업데이트하도록 빌드 시스템을 실행하면 된다. 빌드 시스템은 필요한 탐지를 빌드하거나 다시 빌드하고, 그 결과를 사용해 프로젝트 소스 코드를 빌드한다. strl*() 사례에서는 함수 탐지가 실패했을 때 strlcpy.c와 strlcat.c 대체 구현을 컴파일하고 링크할 수 있다.

탐지에 제대로 된 빌드 시스템을 사용하는 또 다른 장점은 탐지 결과 사이에 의존성을 설정할 수 있다는 점이다. 예를 들어 <string.h>가 없다면 strl*() 함수를 탐지하느라 시간을 낭비할 이유가 없다.

다만 걸림돌이 하나 있다. 빌드파일을 불러오고 평가할 때 탐지 결과를 알아야 한다. 예를 들어 빌드파일 정의를 평가하는 중에 strlcpy.c와 strlcat.c를 빌드에 포함할지 결정한다. 다음은 GNU make를 이용한 예시다.

```

hello: hello.o

hello.o: hello.c

ifndef have_strlcpy

hello: strlcpy.o

strlcpy.o: strlcpy.c

else

CPPFLAGS += -DHAVE_STRLCPY

endif

ifndef have_strlcat

hello: strlcat.o

strlcat.o: strlcat.c

else

CPPFLAGS += -DHAVE_STRLCAT

endif

```

이 예시에서 have_strlcpy와 have_strlcpy의 값은 make가 빌드파일을 평가할 때 이미 알려져 있어야 한다. 하지만 해당 탐지가 전체 빌드의 일부라면, 빌드파일 평가가 끝나고 make가 실제 타깃 빌드를 시작한 뒤에야 값이 결정된다.

빌드파일 로딩을 일시 중지하고, 특정 타깃을 업데이트한 다음, 그 결과를 빌드파일에 불러오고 나서 로딩을 재개할 수 있다면 이 문제는 간단히 해결할 수 있다.

GNU make에는 이 기능의 변형이 있다. include 지시문으로 지정한 makefile이 없거나 오래된 경우 make가 이를 업데이트하려고 한다. 그러나 이 기능에는 아쉬운 점이 두 가지 있다. 첫째, make는 include 지시문을 만났을 때 멈춰서 makefile을 업데이트하지 않는다. 존재하지 않거나 오래된 포함 makefile은 무시하고 끝까지 평가한 다음에야 업데이트를 시도한다. 따라서 탐지 결과가 아직 없거나 오래된 경우도 빌드파일이 처리할 수 있어야 한다. 둘째, 포함된 makefile이 하나라도 업데이트되면 make가 처음부터 makefile 로딩을 다시 시작한다. 규모가 큰 프로젝트에서는 상당한 성능 부담이 될 수 있다.

build2는 이런 단점 없이 빌드파일 로딩 중 업데이트를 제대로 지원한다. 빌드파일 평가를 멈추고 관련 타깃을 모두 업데이트한 다음 결과를 불러오고, 재시작 없이 로딩을 이어간다. 이 기능으로 설정 탐지를 주 빌드 과정에 통합했으며, 만족스러운 결과를 얻었다. 성능 수치는 아래에 소개한다.

빌드 시스템에서 로딩 중 업데이트를 지원하면 빌드 과정에 정보를 전달하고 알아내야 할 여러 필요가 드러난다. 예를 들어 이 기능으로 C 또는 C++ 컴파일러의 사전 정의 매크로를 추출해 빌드파일 평가 중 변수로 사용할 수 있다.

취약성 문제를 다루기 전에 관련된 다른 세부 사항을 살펴보자. autoconf와 CMake는 함수 탐지를 위해 테스트 프로그램을 컴파일하고 링크한다. 이때 헤더에 함수 선언이 있는지 확인하지 않고, 함수 선언을 직접 작성한다. 즉 실제로 확인하는 것은 라이브러리에 대응하는 심볼이 있는지다. 이 방식에는 여러 예외와 단점이 있다. 함수가 인라인 함수이거나 컴파일러 내장 함수라면 심볼이 없을 수 있다. 심볼은 있어도 해당 함수 선언이 헤더에서 활성화되지 않을 수 있다. 함수 서명이 예상과 다르면 실제 호출 코드가 유효하지 않을 수도 있다.

glibc 2.38을 구체적인 예로 들어보자. 자체 strlcpy() 선언을 넣은 탐지 프로그램은 _GNU_SOURCE 없이도 링크에 성공한다. 하지만 <string.h>의 strlcpy() 선언은 컴파일 중 이 매크로를 정의해야 사용할 수 있다.

라이브러리 심볼의 존재를 확인하는 대신 표준 헤더를 포함해 함수 선언을 가져온 뒤, 실제 호출 코드가 컴파일되는지 확인하는 방법도 있다. 이 방식은 앞서 언급한 예외를 피하고 실제 코드에서 함수를 사용하는 방식과도 더 가깝다. C 탐지를 컴파일할 때 사용이 중단된 암시적 함수 선언을 비활성화해야 하지만, 최신 C 컴파일러에서는 어렵지 않다. 따라서 함수 탐지에는 호출 코드 컴파일 방식을 권장한다. GCC와 Clang에서는 -fsyntax-only, MSVC에서는 /Zs를 사용해 속도를 크게 높일 수 있다는 장점도 있다.

취약성 문제를 해결하기는 어렵다. 핵심은 탐지 대상 기능의 부재로 인한 실패와 다른 모든 실패를 구분하는 것이다. 이를 직접 처리하려면 컴파일러 진단 메시지를 분석해야 하는데, 현실적으로 해결하기 어렵다.

컴파일러마다 진단 메시지가 다르고 버전이 바뀌면 표현도 달라질 수 있다. 기능 부재를 나타낼 수 있는 탐지 실패 유형도 헤더 누락, 함수 선언 누락, 여러 형태의 매개변수나 인수 불일치, 반환값 불일치 등 다양하다. 결국 계속 늘어나는 컴파일러와 버전마다 진단 패턴 목록을 관리해야 한다.

가능성을 높이는 한 가지 방법은 컴파일러 버전을 하나로 제한하는 것이다. 최근 공개된 EDG 컴파일러 프런트엔드가 이런 용도로 쓰일 수 있을지도 모른다.

차선책은 ‘대조’ 탐지를 두는 것이다. 원래 탐지와 거의 같은 상황에서 실패하되, 확인하려는 기능이 없는 경우에만 다르게 동작할 대조용 변형을 만든다. 대조 탐지는 원래 탐지를 최대한 똑같이 흉내 내야 한다. 같은 헤더를 포함하고 같은 언어 구문과 논리를 사용해야 한다. 두 변형을 같은 소스 파일에 구현하는 편이 가장 좋다. strlcpy() 탐지는 다음과 같이 작성할 수 있다.

```

#include <string.h>

size_t f (void)

{

char dst[8];

#ifndef CONTROL

size_t n = sizeof (dst);

size_t r = strlcpy (dst, "strlcpy", n);

#else

strcpy (dst, "strlcpy");

size_t r = 7;

#endif

return r;

}

```

이 방법으로 탐지를 실행할 때는 두 단계가 필요하다.

  • 대조 탐지를 컴파일한다. 진단 메시지를 그대로 전달하고 컴파일이 실패하면 탐지도 실패시킨다. 이 과정에서 헤더 의존성 정보도 추출한다.
  • 실제 탐지를 컴파일하되 진단 메시지는 무시한다. 컴파일에 성공하면 기능이 있다고 보고, 실패하면 없다고 판단한다.

대조 방식이 영리한 해결책처럼 보일 수 있지만 단점도 있다. 적절한 대조 탐지를 작성하기 어려울 수 있다는 점이 가장 크다. strlcpy()의 경우 strcpy()를 사용해 매우 비슷한 대조 탐지를 만들기 쉽다. 이를 통해 포함한 헤더가 존재하고 사용할 수 있는지, 언어 구문과 논리가 올바른지 등을 확인할 수 있다. 하지만 다른 상황에서는 원래 탐지에 가까운 대조를 만들기가 더 어려울 수 있다.

이 개선 사항들은 build2에서 시험했다. 자세한 내용은 HOWTO 문서와 예시를 참고할 수 있다. 전체 접근 방식의 성능도 평가했다. Intel i9-12900K 같은 최신 하드웨어에서 대조 탐지를 포함한 탐지 500개를 실행하는 데 드는 오버헤드는 약 0.5초다.