원글: https://arca.live/b/programmer/86258136?p=1


1. a = "ddd"

우선 좌변은 char 타입(정수), 우변은 char[4]인 배열 타입

배열은 값으로 평가될때 포인터로 붕괴(decay)함

따라서 포인터를 정수에 대입하려는 식

그러면 (x86_64의 경우) 포인터가 크기가 8인 정수로 변환되고, 이거를 크기가 1인 정수에 대입하므로 "일반적으로" LSB쪽의 1바이트만 잘려서 대입 됨

즉 a의 값은 "ddd" 문자열이 저장된 "주소"의 하위 1바이트 ('d'가 아니라 """주소""")


2. printf("%s", a)

우선 a가 가변 인자 함수로 넘어갈 때, 기본적으로 int보다 작은 타입들은 int로 캐스팅되어 들어감

따라서 a 값과 같은 값을 가지는 int형 정수로 넘어감

%s는 char *를 받아서 문자열을 출력해주는데, 이 때 전달된 포인터가 NULL일 경우 (null)을 출력해주는 C Library가 있음. 대표적으로 glibc


즉 우연인지 필연인지 "ddd"가 저장된 주소의 하위 1바이트가 0x00이라서 a의 값이 0이었고, 이를 NULL로 인식한 printf가 (null)을 출력해주는 것


과연 정말인지 확인해보자

환경은 mingw-w64-ucrt-x86_64, 컴파일러는 gcc


위 코드는 "ddd" 문자열이 저장되는 주소와 a에 대입된 값을 알아보기 위해 ptr과 a의 값을 각각 포인터와 정수형으로 출력하였다

얘를 컴파일&실행 해보면


보시다시피 "ddd"가 저장된 주소는 0x0000_7FF7_E25D_40'00'이고 따옴표 친 최하위 바이트 0x00이 a에 저장된 것을 볼 수 있다.

그렇다면 만약 문자열의 주소가 0x00으로 끝나지 않는다면 어떤 일이 벌어질까? 그리고 최상위 바이트도 00인데 정말 최하위 바이트가 저장되는걸까?


문자열의 시작주소 대신 시작주소 + 1을 대입해보자


그러면 보다시피 주소가 0x0000_7FF7_4802_40'01'이 되고 최하위 바이트인 0x01이 a에 저장된다.

그런데 주소값 0x0000_0000_0000_0001은 NULL도 아니고(즉 "(null)"을 출력하려 하지 않고) 읽기 가능한 메모리 영역도 아니기 때문에 읽으려는 시도가 실패하고 segmentaion fault가 발생한다.


호기심 해결!

지적환영 태클환영 오타 및 맞춤법 지적도 대환영



*) long -> char 같은 더 작은 타입으로의 캐스팅은 unsigned냐 signed냐에 따라 표준에 써있는 내용이 다르다.

unsigned일 경우에는 직관적으로 하위 바이트를 잘라 넣고(== 2의 거듭제곱으로 modular 연산한 결과), signed의 경우에는 implementation defined, 즉 구현에 따라 동작이 바뀔 수 있다. 이 경우에는 그냥 unsigned랑 동일하게 하위 바이트를 잘라 넣는 것 처럼 보이지만 이것에 의존하는 코드는 좋지 않다.


undefined behavior와 implementation defined는 비슷해 보이지만 다르다. UB는 동작이 표준에 정의되어있지 않고 구현에서 문서화 될 필요가 없으며, impl def는 동작을 표준에서 정하지는 않지만 구현에 따라 일관된 동작을 하며 문서화되어야한다.

즉 사용이 금기시되는 UB와 달리 long -> char같은 구현 정의 동작은 구현마다 다를 수는 있지만 문서화는 되어있으며, 주의깊게 사용하여야한다.


**) int와 signed int는 완전히 동일한 타입이지만 char는 signed char 또는 unsigned char과 동등한 타입으로 정의되어있으며, 둘 중 어느 하나와 동등하더라도 서로 같은 타입은 아니다.

즉 int는 signedness에 따라 int == signed int, unsigned int의 두가지 타입이 있지만 char는 char, signed char, unsigned char의 세가지 타입이 별개로 존재한다. short, long, long long은 int와 마찬가지


***) clang에서는 1번을 경고가 아닌 에러로 처리하므로 -Wno-int-conversion같은 옵션을 줘서 에러를 끄거나, 경고로 바꿔야 컴파일에 성공함.

링크: https://reviews.llvm.org/D129881


수정) a가 함수로 넘어갈 때 -> a가 '가변 인자' 함수로 넘어갈 때