왜 printf ( "% f", 0); 정의되지 않은 동작을 제공합니까?


87

진술

printf("%f\n",0.0f);

0을 인쇄합니다.

그러나 진술

printf("%f\n",0);

임의의 값을 인쇄합니다.

나는 내가 어떤 종류의 정의되지 않은 행동을 보이고 있다는 것을 알고 있지만, 그 이유를 구체적으로 알 수는 없습니다.

모든 비트가 0으로되는 부동 소수점 값은 여전히 유효 float0의 값
floatint(즉, 더욱 중요한 경우) 내 시스템의 동일한 크기이다.

부동 소수점 리터럴 대신 정수 리터럴을 사용 printf하면이 동작이 발생 하는 이유는 무엇 입니까?

PS를 사용하면 동일한 동작을 볼 수 있습니다.

int i = 0;
printf("%f\n", i);

37
printf는을 (를) 예상하고 double있으며 int. floatint컴퓨터에 같은 크기 일 수 있지만 0.0f실제로 변환되는 double가변 인자 인수 목록에 밀려 (때 printf기대하는). 요컨대, printf사용하는 지정자와 제공하는 인수를 기반으로하여 협상의 끝을 이행하지 않습니다 .
WhozCraig

22
Varargs 함수는 할 수 없기 때문에 함수 인수를 해당 매개 변수의 유형으로 자동 변환하지 않습니다. 프로토 타입이있는 비 varargs 함수와 달리 필요한 정보는 컴파일러에서 사용할 수 없습니다.
EOF

3
우 ... "변형." 난 그냥 ... 새로운 단어를 배웠습니다
마이크 로빈슨에게


3
시도 할 다음 일은 전달하는 (uint64_t)0대신 0당신은 여전히 임의 행동 수 (가정을 있는지 여부를 확인 double하고 uint64_t동일한 크기와 정렬을 가지고). 다른 레지스터에서 다른 유형이 전달되기 때문에 일부 플랫폼 (예 : x86_64)에서는 출력이 여전히 무작위 일 수 있습니다.
Ian Abbott

답변:


121

"%f"형식은 형식의 인수가 필요합니다 double. 유형의 인수를 제공합니다 int. 그것이 행동이 정의되지 않은 이유입니다.

이 표준은 모든 비트 제로는 유효한 표현은 보장하지 않습니다 0.0(이 종종 있지만), 또는의 double값, 또는 그 intdouble(그것의 기억 같은 크기 double,하지 float가 같은 경우에도, 나) 동일한 방식으로 가변 함수에 인수로 전달됩니다.

시스템에서 "작동"할 수 있습니다. 이는 정의되지 않은 동작의 최악의 증상입니다. 오류를 진단하기 어렵 기 때문입니다.

N1570 7.21.6.1 단락 9 :

... 인수가 해당 변환 사양에 대해 올바른 유형이 아닌 경우 동작이 정의되지 않습니다.

유형의 인수가 float승격되는 double이유입니다, printf("%f\n",0.0f)작동합니다. 또는로 int승격되는 것보다 좁은 정수 유형의 인수 . 이러한 프로모션 규칙 (N1570 6.5.2.2 단락 6에 의해 지정됨)은 .intunsigned intprintf("%f\n", 0)

인수 0를 예상하는 비가 변 함수에 상수 를 전달 double하면 함수의 프로토 타입이 표시 된다는 가정하에 동작이 잘 정의됩니다. 예를 들어, sqrt(0)(after #include <math.h>)는 인수 0int에서 double- 로 암시 적으로 변환합니다 . 컴파일러는 인수 가 필요 sqrt하다는 선언에서 볼 수 있기 때문 double입니다. 에 대한 정보가 없습니다 printf. 와 같은 가변 함수 printf는 특별하며 호출을 작성하는 데 더 많은주의가 필요합니다.


13
여기에 몇 가지 훌륭한 핵심 포인트가 있습니다. 첫째,이 있다고 double하지 float영업 이익의 폭 가정 않을 수 없습니다 (아마하지 않습니다) 보류 그래서. 둘째, 정수 0과 부동 소수점 0이 동일한 비트 패턴을 갖는다는 가정도 유지되지 않습니다. 좋은 일
궤도

2
@LucasTrzesniewski : 좋아,하지만 내 대답이 어떻게 질문을 던지는 지 모르겠다. 나는 이유를 설명하지 않고 float승진 된 상태를했다 double. 그러나 그것이 요점은 아니었다.
Keith Thompson

2
@ robertbristow - 존슨 : 컴파일러는 특별 후크를 할 필요가 없습니다 printfGCC하지만, 예를 들어, (일부는 진단 할 수 있도록 오류를 가지고, 경우 형식 문자열 리터럴입니다). 컴파일러는 printffrom 선언을 볼 수 <stdio.h>있는데, 이는 첫 번째 매개 변수가 a const char*이고 나머지는로 표시됨을 알려줍니다 , .... 아니, %f위한 double(그리고 float로 승격 double)을 %lf위한 것입니다 long double. C 표준은 스택에 대해 아무것도 말하지 않습니다. printf올바르게 호출 될 때만 동작을 지정 합니다.
Keith Thompson

2
@ robertbristow-johnson : 예전에는 "lint"가 gcc가 수행하는 추가 검사를 자주 수행했습니다. A는 float전달 printf로 승격됩니다 double; 마법 같은 것은 없습니다. 가변 함수를 호출하기위한 언어 규칙 일뿐입니다. printf호출자 가 전달 한다고 주장한 형식 문자열을 통해 자체적으로 알고 있습니다. 해당 주장이 잘못된 경우 동작은 정의되지 않습니다.
키이스 톰슨

2
작은 정정 : l길이 개질제 "는 다음에 아무런 영향을 미치지 않는다 a, A, e, E, f, F, g, 또는 G변환 지정자"는 길이 대 개질제 long double변환이다 L. (@ robertbristow - 존슨도 관심이있을 수 있음)
다니엘 피셔

58

여러 다른 답변에에 감동하지만 같은 첫째, 충분히 명확하게 밖으로 철자 내 마음에 : 그것은 수행 의 정수 제공하는 일을 대부분의 라이브러리 함수가 걸리는 상황 double이나 float인수를. 컴파일러는 자동으로 변환을 삽입합니다. 예를 들어, sqrt(0)는 잘 정의되어 있고 정확히으로 작동 sqrt((double)0)하며 여기에서 사용되는 다른 정수 유형 표현식에 대해서도 마찬가지입니다.

printf은 다르다. 가변적 인 수의 인수를 취하기 때문에 다릅니다. 기능 프로토 타입은 다음과 같습니다.

extern int printf(const char *fmt, ...);

따라서 당신이 쓸 때

printf(message, 0);

컴파일러에는 두 번째 인수가 어떤 유형이 printf 될 것으로 예상 하는지에 대한 정보가 없습니다 . 인수 표현식의 유형 () 만 int있습니다. 따라서 대부분의 라이브러리 함수와 달리 인수 목록이 형식 문자열의 예상과 일치하는지 확인하는 것은 프로그래머의 책임입니다.

(최신 컴파일러 형식 문자열을 조사하여 유형 불일치가 있음을 알려줄 수 있지만, 의미 한 바를 달성하기 위해 변환 삽입을 시작하지는 않을 것입니다. , 덜 유용한 컴파일러로 다시 빌드했을 때보 다 몇 년 후.)

이제 질문의 나머지 절반은 다음과 같습니다. 대부분의 최신 시스템에서 (int) 0과 (float) 0.0이 모두 0 인 32 비트로 표시된다는 점을 감안할 때 우연히 작동하지 않는 이유는 무엇입니까? C 표준은 "이것은 작동하는 데 필요하지 않습니다. 당신은 스스로 할 수 있습니다."라고 말하고 있지만 작동하지 않는 가장 일반적인 두 가지 이유를 설명하겠습니다. 이것이 왜 필요하지 않은지 이해하는 데 도움이 될 것입니다 .

첫째, 역사적 이유로 float변수 인수 목록 을 통과하면 대부분의 최신 시스템에서 64 비트 너비 인로 승격 됩니다 . 따라서 64 개를 예상하는 수신자에게 32 개의 0 비트 만 전달합니다.doubleprintf("%f", 0)

두 번째로 중요한 이유는 부동 소수점 함수 인수가 정수 인수 와 다른 위치에 전달 될 수 있다는 것입니다. 예를 들어, 대부분의 CPU에는 정수 및 부동 소수점 값에 대한 별도의 레지스터 파일이 있으므로 인수 0 ~ 4가 정수인 경우 레지스터 r0 ~ r4에 들어가고, 부동 소수점 인 경우 f0 ~ f4에 들어가는 것이 규칙 일 수 있습니다. 따라서 printf("%f", 0)레지스터 f1에서 0을 찾습니다.하지만 전혀 없습니다.


1
일반 함수에 레지스터를 사용하는 아키텍처에서도 가변 함수에 레지스터를 사용하는 아키텍처가 있습니까? 다른 함수 (float / short / char 인수가있는 함수 제외)를으로 선언 할 수 있지만 가변 함수를 올바르게 선언해야하는 이유라고 생각했습니다 ().
Random832 jul.

3
@ Random832 요즘 가변 함수와 일반 함수의 호출 규칙의 유일한 차이점은 제공된 인수의 실제 개수와 같이 가변에 추가 데이터가 제공 수 있다는 것 입니다. 그렇지 않으면 모든 것이 정상적인 기능과 똑같은 위치에 들어갑니다. 예를 들어 x86-64.org/documentation/abi.pdf의 섹션 3.2를 참조하십시오 . 여기서 가변성 에 대한 유일한 특수 처리는에서 전달 된 힌트입니다 AL. (예, 이는의 구현 va_arg이 예전보다 훨씬 더 복잡하다는 것을 의미합니다 .)
zwol

@ Random832 : 저는 항상 그 이유가 일부 아키텍처에서 알려진 수와 유형의 인수를 가진 함수를 특수 지침을 사용하여보다 효율적으로 구현할 수 있기 때문이라고 생각했습니다.
celtschk

@celtschk SPARC 및 IA64의 "등록 창"을 생각할 수 있습니다. 이는 소수 의 인수 로 함수 호출의 일반적인 경우를 가속화하도록되어 있습니다 (실제로는 그 반대입니다). 컴파일러가 가변 함수 호출을 특별히 처리 할 필요가 없습니다. 호출 사이트 의 인수 수가 호출 수신자가 가변인지 여부에 관계없이 항상 컴파일 타임 상수이기 때문입니다.
zwol

@zwol : 아니요, 하드 코딩 된 정수가있는 ret n8086 의 명령을 생각하고 n있었기 때문에 가변 함수에 적용 할 수 없었습니다. 그러나 C 컴파일러가 실제로 그것을 활용했는지는 모르겠습니다 (비 C 컴파일러는 확실히했습니다).
celtschk

13

일반적으로를 예상하는 함수를 호출하지만를 double제공 int하면 컴파일러가 자동으로으로 변환됩니다 double. printf인수의 유형이 함수 프로토 타입에 지정되지 않았기 때문에에서는 발생하지 않습니다 . 컴파일러는 변환이 적용되어야한다는 것을 알지 못합니다.


4
또한 printf() 특히 인수가 모든 유형이 될 수 있도록 설계되었습니다. format-string의 각 요소에서 예상되는 유형을 알고 있어야하며 올바르게 제공해야합니다.
Mike Robinson

@MikeRobinson : 음, 모든 원시 C 유형. 가능한 모든 유형의 매우 작은 하위 집합입니다.
MSalters

13

부동 리터럴 대신 정수 리터럴을 사용하면 왜 이런 동작이 발생합니까?

때문에 printf()이외의 매개 변수를 입력하지 않습니다 const char* formatstring제 1 회 하나. ...나머지는 모두 c 스타일 줄임표 ( )를 사용합니다.

형식 문자열에 지정된 형식 지정 유형에 따라 전달 된 값을 해석하는 방법을 결정합니다.

당신은 시도 할 때와 같은 종류의 정의되지 않은 행동을 할 것입니다.

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

3
의 일부 특정 구현은 printf이러한 방식으로 작동 할 수 있습니다 (전달 된 항목이 주소가 아니라 값이라는 점 제외). C 표준은 다른 가변 함수의 작동 방식을 지정하지 않고 printf동작 만 지정합니다. 특히 스택 프레임에 대한 언급은 없습니다.
Keith Thompson

작은 문제 : 유형이 지정된 매개 변수 인 형식 문자열 printf하나const char* 있습니다. BTW, 질문에는 C와 C ++ 모두 태그가 지정되어 있으며 C는 실제로 더 관련성이 있습니다. 나는 아마도 reinterpret_cast예제로 사용하지 않았을 것입니다 .
Keith Thompson

흥미로운 관찰 : 동일한 정의되지 않은 동작, 동일한 메커니즘으로 인해 가능성이 높지만 세부적인 차이가 있습니다. 질문에서와 같이 int를 전달 하면 int를 double로 해석하려고 할 때 printf 에서 UB가 발생 합니다. , 이미 일이 외부 PF 역 참조 할 때 ...
아콩 카과

@Aconcagua 설명이 추가되었습니다.
πάντα ῥεῖ

이 코드 샘플은 엄격한 앨리어싱 위반에 대한 UB이며 질문이 묻는 것과 완전히 다른 문제입니다. 예를 들어 부동 소수점이 다른 레지스터에서 정수로 전달 될 가능성을 완전히 무시합니다.
MM

12

일치하지 않는 printf()지정자 "%f"와 유형을 사용 (int) 0하면 정의되지 않은 동작이 발생합니다.

변환 사양이 유효하지 않으면 동작이 정의되지 않습니다. C11dr §7.21.6.1 9

UB의 후보 원인.

  1. 그것은 스펙 당 UB이고 컴파일은 고상합니다 .'nuf가 말했습니다.

  2. double그리고 int다른 크기의이다.

  3. doubleint다른 스택을 사용하여 값을 전달할 수있다 (일반적인 대에 FPU의 스택).

  4. (A)은 double 0.0 수있는 모든 제로 비트 패턴으로 정의 할 수 없습니다. (드문)


10

이것은 컴파일러 경고에서 배울 수있는 좋은 기회 중 하나입니다.

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n",0);
  ^

또는

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

따라서 printf호환되지 않는 유형의 인수를 전달하기 때문에 정의되지 않은 동작이 생성됩니다.


9

무엇이 헷갈리는 지 잘 모르겠습니다.

형식 문자열에는 double; 대신 int.

두 유형이 동일한 비트 폭을 갖는지 여부는 완전히 관련이 없습니다. 단, 이와 같이 손상된 코드에서 하드 메모리 위반 예외가 발생하지 않도록하는 데 도움이 될 수 있습니다.


3
@Voo : 그 형식 문자열 수정이 되고 불행하게도 이름,하지만 당신이라고 생각 거라고 난 아직도 왜 표시되지 않습니다 int여기에 허용 될 것이다.
궤도의 가벼운 경주

1
@Voo : "(유효한 플로트 패턴으로도 한정됩니다)"int유효한 플로트 패턴으로 한정됩니까? 2의 보수와 다양한 부동 소수점 인코딩은 거의 공통점이 없습니다.
궤도의 가벼운 경주

2
대부분의 라이브러리 함수에서 0입력 된 인수에 정수 리터럴 을 제공 double하면 올바른 작업을 수행 하기 때문에 혼란 스럽습니다 . 초보자에게는 컴파일러가에서 printf주소가 지정된 인수 슬롯에 대해 동일한 변환을 수행하지 않는다는 것이 분명하지 않습니다 %[efg].
zwol

1
@Voo : 이것이 얼마나 끔찍하게 잘못 될 수 있는지에 관심이 있다면 x86-64 SysV ABI에서 부동 소수점 인수가 정수 인수와 다른 레지스터 세트로 전달된다는 것을 고려하십시오.
EOF

1
@LightnessRacesinOrbit 나는 어떤 것이 UB 인 이유 를 논의 하는 것이 항상 적절 하다고 생각합니다. 일반적으로 어떤 구현 관용도가 허용되고 일반적인 경우에 실제로 일어나는 일에 대해 이야기하는 것을 포함합니다.
zwol

4

"%f\n"두 번째 printf()매개 변수의 유형이 인 경우에만 예측 가능한 결과를 보장 double합니다. 다음으로 가변 함수의 추가 인수는 기본 인수 승격의 대상입니다. 정수 인수는 정수 승격에 해당하므로 부동 소수점 유형 값이 생성되지 않습니다. 그리고 float매개 변수로 승격된다 double.

결론적으로 표준은 두 번째 인수가 또는 float또는 double아무것도 허용하지 않습니다.


4

공식적으로 UB 인 이유는 이제 여러 답변에서 논의되었습니다.

이 동작이 특별히 발생하는 이유는 플랫폼에 따라 다르지만 아마도 다음과 같습니다.

  • printf표준 vararg 전파에 따라 인수를 예상합니다. 즉, a float는 a 가되고 a double보다 작은 것은 intwill이됩니다 int.
  • int함수가 예상 하는 위치를 전달 하고 있습니다 double. 귀하는 int귀하의 아마 32 비트이며 double64 비트. 즉, 인수가 있어야하는 위치에서 시작하는 4 개의 스택 바이트는 0이지만 다음 4 바이트에는 임의의 내용이 있습니다. 이것이 표시되는 값을 구성하는 데 사용되는 것입니다.

0

이 "결정되지 않은 값"문제의 주요 원인은 매크로가 수행하는 유형 의 포인터에 int대한 printf변수 매개 변수 섹션에 전달 된 값 의 포인터 캐스트 에 double있습니다 va_arg.

이것은 double크기 메모리 버퍼 영역이 int크기 보다 크기 때문에 printf에 매개 변수로 전달 된 값으로 완전히 초기화되지 않은 메모리 영역을 참조하게합니다 .

따라서이 포인터가 역 참조 될 때 결정되지 않은 값이 반환되거나에 매개 변수로 전달 된 값을 부분적으로 포함하는 "값"이 반환 printf되며 나머지 부분의 경우 다른 스택 버퍼 영역 또는 코드 영역 ( 메모리 오류 예외 발생), 실제 버퍼 오버플로 .


"printf"및 "va_arg"의 샘플 코드 구현의 이러한 특정 부분을 고려할 수 있습니다 ...

printf

va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 


이중 값 매개 변수 코드 케이스 관리의 vprintf (gnu impl 고려)의 실제 구현은 다음과 같습니다.

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);



참조

  1. "printf"(vprintf))의 gnu 프로젝트 glibc 구현
  2. printf의 증폭 코드 예
  3. va_arg의 증폭 코드 예
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.