Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/C언어

[C언어] stdint.h 를 통한 타입 작성과 CLANG Header 읽기 개고생

sm_amoled 2025. 8. 24. 17:29

키워드

type, stdint.h, implementation-defined behaviour, int32_t, 이식성

타입을 정확하게 쓰기

여러 임베디드 샘플코드를 보면 타입이 단순히 int 처럼 작성하는게 아니라, int32_t 이렇게 작성하는 것을 볼 수 있다. 이게 더욱 정확하게 bit수까지 포함해 타입을 작성하기 때문에 정확한 개발이 가능하고, 이후 컴파일러나 환경 변화로 인해 타입 별 bit 수가 변경되더라도 손쉽게 핸들링이 가능하기 때문에 유용한 방법이라고 한다. ( = 이식성을 높이기)

이걸 자동으로 해주는게 stdint.h 헤더파일이다.

stdint.h 헤더를 통해 타입을 define하는 경우, 코드에서 sign여부와 길이를 정확하게 명시한 타입을 사용할 수 있다.

아래는 xcode에서 stdlib.h를 불러온 다음, 코드를 확인한 내용이다.

// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.

#ifdef __INT32_TYPE__

# ifndef __int8_t_defined /* glibc sys/types.h also defines int32_t*/

// ***** 이렇게 int32_t의 타입을 선언해주고 있다!!! *****
typedef __INT32_TYPE__ int32_t;

# endif /* __int8_t_defined */

# ifndef __uint32_t_defined  /* more glibc compatibility */
# define __uint32_t_defined
typedef __UINT32_TYPE__ uint32_t;
# endif /* __uint32_t_defined */

# undef __int_least32_t
# define __int_least32_t int32_t
...
#endif /* __INT32_TYPE__ */

위 코드를 살펴보면, int32_t라는 이름으로 타입을 정해주고 있다. 그런데 타입의 원본은 __INT32_TYPE__ 이라는 녀석으로 되어있다. 오호라. 그런데 이 __INT32_TYPE__ 이라는 값에 대한 정의는 찾아볼 수 없었다. 검색해보니 요건 컴파일러가 알아서 넣어주는 값이라고 한다. 아마도 argument로 넣어주는 값인듯? 그래서 나중에 int의 크기가 64bit으로 변경된다고 하더라도 컴파일러는 int32_t 라는 타입에 대해 정확히 32bit인 정수형 타입으로 변환해줄 것이기 때문에 타입에 안정성이 생기고 이식성이 좋아진다고 한다.

사실 이걸 뜯어본 이유는 윈도우의 Visual Studio에서 stdint.h 를 뜯어봤을 때는 다른 내용이 작성되어 있었기 때문이다.

/* Copyright (C) 1997, 1998, 1999, 2000, 2001 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   ...

/*
 *    ISO C99: 7.18 Integer types <stdint.h>
 */

#ifndef _STDINT_H
#define _STDINT_H    1

...

/* There is some amount of overlap with <sys/types.h> as known by inet code */
#ifndef __int8_t_defined
# define __int8_t_defined
typedef signed char        int8_t;
typedef short int        int16_t;

// ***** 이렇게 int32_t의 타입을 선언해주고 있다!!! *****
typedef int            int32_t;

# if __WORDSIZE == 64
typedef long int        int64_t;
# else
__extension__
typedef long long int        int64_t;
# endif
#endif

/* Unsigned.  */
typedef unsigned char        uint8_t;
typedef unsigned short int    uint16_t;
#ifndef __uint32_t_defined
typedef unsigned int        uint32_t;
# define __uint32_t_defined
#endif
#if __WORDSIZE == 64
typedef unsigned long int    uint64_t;
#else
__extension__
typedef unsigned long long int    uint64_t;
#endif

중간에 표시해둔 곳을 보면, int32_t 라는 타입에 대해 단순히 int 타입의 이름을 재지정한 것과 별반 다르지 않게 정의되어있다.

🤔🤔🤔🤔🤔🤔 이러면 나중에 int가 64bit로 변경된다면 int32_t 타입도 64bit으로 변경되어서 타입이 아예 틀어지는게 아닌가? 라는 의심이 들었다.

그리고 클로드와 함께 검색을 해보니 내가 우려한 바가 어느정도 일리있다는 이야기를 하는 것 같았다.

“Claude가 생성해준 답변”

두 방식의 비교

LLVM/Clang 방식의 장점:

  • 자동 적응: 컴파일러가 알아서 올바른 타입 선택
  • 미래 지향적: 새로운 아키텍처에서도 자동 동작
  • 유지보수 불필요: 헤더 파일 수정 없이도 이식성 보장

glibc 방식의 단점:

  • 수동 관리: 새로운 플랫폼마다 헤더 수정 필요
  • 깨지기 쉬움: 가정이 틀어지면 타입 크기가 바뀜
  • 유지보수 부담: 플랫폼별 조건부 컴파일 코드 관리

 

다만 int가 이제 32bit로 거의 고정이 되어서 거의 변경될 일은 없다고는 하지만, long 같은 타입은 여전히 변경될 여지가 남아있어 LLVM의 방식 (Xcode 헤더)이 더욱 미래지향적이고 견고한 설계방식이라고 한다.

그러면 GNU 에서는 버전을 올릴때마다 헤더파일들도 새로 배포를 하는건가? (아니, 원래 그런 방식으로 하니깐 현재 버전에서 타입을 분리하지 않아도 상관이 없는걸까?)

다른 타입도 있었다

길이가 2의 거듭제곱 형태만 있는게 아니였다.

  • int40_t, int48_t, int56_t 같은 조금은 아리송한 타입들도 있었다.
  • 요런 타입들은 주로 특수 HW의 연산 지원, 네트워크 프로토콜 등을 위해 제공된다. 시스템 프로그래밍, 임베디드 분야에서는 간간히 볼 일이 있을지도?
    • 40bit ⇒ 고정소수점 연산, 물리적인 주소, 파일 시스템 번호 등에 활용
    • 48bit ⇒ 네트워크 MAC 주소 표현에 사용
    • 56bit ⇒ 타임스템프, DB key 등에 사용

헤더를 뜯어보다보니, int_least32_t 같은 타입이나 int_fast32_t 라는 이름을 가진 타입들도 등장했다.

#ifdef __int_least32_t
typedef __int_least32_t int_least32_t;
typedef __uint_least32_t uint_least32_t;
typedef __int_least32_t int_fast32_t;
typedef __uint_least32_t uint_fast32_t;
#endif /* __int_least32_t */
  • least 타입
    • least 타입은 지정한 타입의 길이를 최소한으로 가지는 가장 작은 길이의 타입이다.
    • 메모리를 효율적으로 사용할 수 있는 타입을 자동으로 지정해준다.
    • 32bit 를 요구한다고 해서 정확히 32bit 일 필요는 없고, 조금 더 큰 메모리가 효율적이라면/32bit 자료형이 없다면 해당 메모리로 지정해주게된다.
    • 16bit 시스템에서 int_least32_t 를 찾는다면 long 타입으로 지정해줄 것이다.
  • fast 타입
    • fast 타입은 지정한 타입의 길이를 포함한 연산을 가장 빨리 수행할 수 있는 길이의 타입이다.
    • 보통은 그냥 word 의 크기와 동일한 것 같다.

LLVM의 stdlib.h 헤더에는 요 값들을 아래처럼 정의해주고 있다.

#ifdef __INT32_TYPE__

# ifndef __int8_t_defined /* glibc sys/types.h also defines int32_t*/
typedef __INT32_TYPE__ int32_t;
# endif /* __int8_t_defined */

# ifndef __uint32_t_defined  /* more glibc compatibility */
# define __uint32_t_defined
typedef __UINT32_TYPE__ uint32_t;
# endif /* __uint32_t_defined */

# undef __int_least32_t
# define __int_least32_t int32_t
# undef __uint_least32_t
# define __uint_least32_t uint32_t
# undef __int_least16_t
# define __int_least16_t int32_t
# undef __uint_least16_t
# define __uint_least16_t uint32_t
# undef __int_least8_t
# define __int_least8_t int32_t
# undef __uint_least8_t
# define __uint_least8_t uint32_t
#endif /* __INT32_TYPE__ */

잘 보면, least 32, least 16, least 8 타입들이 모두 int32_t, uint32_t로 지정되어지는 것을 볼 수 있다. (원래 정의를 지우고 새로 덮어쓰기 하는 코드임)

최소 크기가 8bit인 타입을 요청했지만, 이걸 그냥 32bit 타입의 데이터로 적용하는 것을 볼 수 있다. 이렇게 했다면 아마 sizeof를 찍어보면 4byte가 나오겠지?

#include <stdio.h>
#include <stdint.h>

int main(int argc, const char * argv[]) {
    int_least64_t l64;
    int_least32_t l32;
    int_least16_t l16;
    int_least8_t l8;

    int_fast64_t f64;
    int_fast32_t f32;
    int_fast16_t f16;
    int_fast8_t f8;

    printf("sizeof least64: %lu\n", sizeof(l64));
    printf("sizeof least32: %lu\n", sizeof(l32));
    printf("sizeof least16: %lu\n", sizeof(l16));
    printf("sizeof least8 : %lu\n", sizeof(l8));

    printf("sizeof fast64:  %lu\n", sizeof(f64));
    printf("sizeof fast32:  %lu\n", sizeof(f32));
    printf("sizeof fast16:  %lu\n", sizeof(f16));
    printf("sizeof fast8 :  %lu\n", sizeof(f8));
    return 0;
}

>>> 실행결과
sizeof least64: 8
sizeof least32: 4
sizeof least16: 2
sizeof least8 : 1
sizeof fast64:  8
sizeof fast32:  4
sizeof fast16:  2
sizeof fast8 :  1

어째서…?

자 이제 2차전 시작이다.

 

코드 상에서 #include<stdint.h> 를 command+클릭 했을 때 진입할 수 있는 헤더파일(우)과 int_least32_t 를 command+클릭 했을 때 진입할 수 있는 헤더파일(좌)이 다르다.

 

아니 진짜 이건 뭐지. 캬아아아아아악

근데 이렇게 명령어를 입력해보니, stdint.h 헤더파일이 여러개가 있다. 위에 있는 링크와 동일한 걸 보니 요게 여러개 있는게

문제

기능인 듯.

sungmin@bagseongmin-ui-maegbug ~ % echo "#include <stdint.h>" | xcrun clang -v -E -x c - 2>&1 | grep stdint.h
# 1  "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/17/include/stdint.h" 1 3
# 56 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/17/include/stdint.h" 3
# 1  "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdint.h" 1 3 4
...
# 60 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdint.h" 2 3 4
# 57 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/17/include/stdint.h" 2 3

호출구조를 보면 내가 보고 있었던 파일 > 심볼 정의 타입 으로 호출 경로가 나와있다. 그래서 다시 한 번 파일을 열어봤다.

__has_include_next(<stdint.h>) 라는 줄이 있는데, 이게 해당 파일로 보내버리는 코드였다.


#ifndef __CLANG_STDINT_H
// AIX system headers need stdint.h to be re-enterable while _STD_TYPES_T
// is defined until an inclusion of it without _STD_TYPES_T occurs, in which
// case the header guard macro is defined.
#if !defined(_AIX) || !defined(_STD_TYPES_T) || !defined(__STDC_HOSTED__)
#define __CLANG_STDINT_H
#endif

#if defined(__MVS__) && __has_include_next(<stdint.h>)
#include_next <stdint.h>
#else

/* If we're hosted, fall back to the system's stdint.h, which might have
 * additional definitions.
 */
#if __STDC_HOSTED__ && __has_include_next(<stdint.h>)

...
// 파일 제일 마지막에 요렇게 if문을 닫아줌
#endif /* __STDC_HOSTED__ */
#endif /* __MVS__ */
#endif /* __CLANG_STDINT_H */

정리해보자면

  1. 내가 보고있던 LLVM의 헤더파일은 CLANG 기본 헤더파일이다.
  2. 여기 헤더파일을 잘 확인하면, '따로 쓰고싶은 stdint 헤더파일이 있으면 그걸 가져와서 사용하기’ 코드가 있다.
  3. macOS SDK가 플랫폼별 특성을 반영하기 위해 이걸 따로 빌드과정에서 넣어주고 있었다. (당황스러웠던 그 헤더파일)
  4. 만약 이런 플랫폼별 헤더가 없었다면 CLANG의 기본 방식을 적용했을 것이다.

그렇다면, 타겟 플랫폼별로 다른 타입 사이즈를 적용하겠다는 그 설계 철학이 잘 들어맞는 방식인 것 같다. CLANG 인정 ^^

내가 현재 빌드하고있는 맥OS를 위한 헤더파일을 살펴보면 이렇게 되어있다.

#ifndef _STDINT_H_
#define _STDINT_H

#if __LP64__
#define __WORDSIZE 64
#else
#define __WORDSIZE 32
#endif

#include <sys/_types/_int8_t.h>
#include <sys/_types/_int16_t.h>
#include <sys/_types/_int32_t.h>
#include <sys/_types/_int64_t.h>

#include <_types/_uint8_t.h>
#include <_types/_uint16_t.h>
#include <_types/_uint32_t.h>
#include <_types/_uint64_t.h>
...

ㅤㅤ

그리고 여기에서 include 하고 있는 각 파일들을 열어보면 요런식으로 각 타입의 크기를 정해두고 있다. 여기에서는 int8_t 의 크기를 singed char (signed 1byte) 으로 정의하고 있는 것을 볼 수 있다.

#ifndef _INT8_T
#define _INT8_T
typedef signed char           int8_t;
#endif /* _INT8_T */

나중에 만약 실행 플랫폼에 따라 타입을 변경하고 싶다면 (int가 2byte로 사용되는 환경이라면) 여기에 작성되어있는 <sys/_types/_int32_t.h> 파일의 int를 다시 플랫폼에 적합한 형태로 변경해주면 될 것이다!

물론 CLANG의 기본 헤더파일을 참조하지 않고 플랫폼 헤더파일을 사용하였고, 해당 헤더파일에는 40, 48, 56 bit 같은 크기의 자료형은 정의되어있지 않아 include 만으로 사용할 수는 없었다. (필요한 경우에 직접 정의해서 사용하면 될 듯!)

 

덕분에 많이 배웠다… ^,^ 전처리에서 else가 이렇게 이어지는구나. header에서 내용을 원하는 다른 header로 대체할 수 있다는 것도 알게됐다. 후,,, 알고싶지 않았다…

#if

#else

#endif

오늘의 배움

타입에 대한 조금 더 정확한 정의를 파악하고싶다면 #include 에서 헤더파일을 열어보면서 추적하지말고, symbol 추적 (키워드에서 정의로 이동) 을 사용하자.

320x100