이것은 Clang 버그입니다
... 무한 루프를 포함하는 함수를 인라인 할 때. while(1);
메인에 직접 나타날 때 동작이 다르므로 매우 버그가 있습니다.
참조 Arnavion의 대답 @요약 및 링크는 을 . 이 답변의 나머지 부분은 알려진 버그는 물론 버그인지 확인하기 전에 작성되었습니다.
제목 질문에 대답하려면 : 최적화되지 않은 무한 빈 루프를 어떻게 만들 수 있습니까? ? -
만들 die()
매크로가 아닌 기능을 , 연타 3.9 및 이후 버전에서이 버그를 해결할 수 있습니다. 초기 Clang 버전 은 루프call
를 유지하거나 무한 루프를 사용하여 함수의 비 인라인 버전으로 a 를 내 보냅니다 . print;while(1);print;
함수 가 호출자 ( Godbolt )에 인라인 하더라도 안전 해 보입니다 . -std=gnu11
vs.-std=gnu99
아무것도 변경하지 않습니다.
GNU C에만 관심이 있다면 루프 내부의 P__J____asm__("");
도 작동하며이를 이해하는 컴파일러에 대한 주변 코드의 최적화를 손상시키지 않아야합니다. GNU C Basic asm 문장은 암시 적으로volatile
으로 있으므로 C 추상 시스템 에서처럼 여러 번 "실행"해야하는 눈에 보이는 부작용으로 간주됩니다. (그리고 Clang은 GCC 매뉴얼에 설명 된대로 C의 GNU 방언을 구현합니다.)
일부 사람들은 빈 무한 루프를 최적화하는 것이 합법적 일 수 있다고 주장했습니다. 동의하지는 않지만 1을 동의 하더라도 루프가 도달 할 수없는 후에 Clang이 명령문을 가정 하고 실행이 함수의 끝에서 다음 함수 또는 가비지로 넘어가 는 것은 합법적 이지 않습니다. 무작위 명령으로 해독합니다.
(이것은 Clang ++에 대해 표준을 준수하지만 (아직 유용하지는 않지만) 부작용이없는 무한 루프는 C ++에서는 UB이지만 C
는 아닙니다. while (1); C에서 정의되지 않은 동작은? UB는 컴파일러가 기본적으로 모든 것을 방출하도록합니다. asm
루프에 있는 명령문은 C ++에서이 UB를 피할 것이지만 실제로 C ++로 컴파일하는 경우 인라인 할 때를 제외하고는 상수 표현식 무한 빈 루프를 제거하지 않습니다. C로 컴파일)
while(1);
Clang이 컴파일하는 방식을 수동으로 인라인하여 변경 : 무한 루프가 asm에 존재합니다. 이것이 우리가 변호사 변호사 POV에서 기대하는 것입니다.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Godbolt 컴파일러 탐색기 에서 -xc
x86-64의 C ( ) 로 컴파일되는 Clang 9.0 -O3 :
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
동일한 옵션을 가진 동일한 컴파일러 는 같은 것을 먼저 main
호출 하는 a 를 컴파일 하지만 다음에 대한 명령어 방출을 중지합니다.infloop() { while(1); }
puts
main
그 시점 이후에 . 내가 말했듯이, 실행은 함수의 끝에서 다음 함수로 넘어갑니다 (그러나 함수 입력에 대해 스택이 잘못 정렬되어 있기 때문에 유효한 tailcall조차 아닙니다).
유효한 옵션은
- 발광
label: jmp label
무한 루프
- 또는 (무한 루프가 제거 될 수 있음을 승인 한 경우) 두 번째 문자열을 인쇄하기 위해 다른 호출을 보낸 다음
return 0
from main
.
내가 알 수없는 UB가 없으면 C11 구현에 "도달 할 수 없음"을 인쇄하지 않고 충돌하거나 계속 진행하는 것은 분명하지 않습니다.
각주 1 :
레코드에 대해서는 @Lundin의 답변에 동의합니다 .C11 이 비어있는 경우에도 C11이 상수 표현 무한 루프에 대한 종료 가정을 허용하지 않는다는 증거에 대한 표준 을 인용합니다 (I / O, 휘발성, 동기화 또는 기타 없음) 눈에 보이는 부작용).
이것은 일반적인 CPU의 경우 빈 asm 루프로 루프 를 컴파일 할 수있는 조건 세트입니다 . (본문에서 본문이 비어 있지 않은 경우에도 루프가 실행되는 동안 데이터 레이스 UB가 없으면 변수에 대한 할당을 다른 스레드 또는 신호 핸들러에 표시 할 수 없습니다. 따라서 적합한 구현은 원하는 경우 이러한 루프 본문을 제거 할 수 있습니다. 루프 자체를 제거 할 수 있는지에 대한 의문이 남습니다.
C11이 루프 종료를 가정 할 수없고 (UB가 아니라고) 구현할 경우 루프가 런타임에 존재하도록 의도 한 것 같습니다. 무한한 시간에 무한한 양의 작업을 수행 할 수없는 실행 모델로 CPU를 대상으로하는 구현은 빈 상수 무한 루프를 제거 할 정당성이 없습니다. 또는 일반적으로 정확한 표현은 "종료되었다고 가정"할 수 있는지 여부입니다. 루프를 종료 할 수 없으면 수학과 무한대에 대해 어떤 주장을하는지 , 일부 가상 머신에서 무한한 작업을 수행하는 데 걸리는 시간에 관계없이 이후 코드에 도달 할 수 없다는 의미 입니다.
또한 Clang은 단순한 ISO C 호환 DeathStation 9000이 아니라 커널 및 임베디드 기능을 포함한 실제 저수준 시스템 프로그래밍에 유용합니다. 따라서 C11에 대한 제거를 허용 하는 인수를 허용하는지 여부에 관계없이 while(1);
Clang이 실제로 그렇게하고 싶어한다는 것은 의미가 없습니다. 글을 쓰면 while(1);
사고가 아니었을 것입니다. 실수로 무한히 끝나는 루프 (런타임 변수 제어 표현식 사용)를 제거하는 것이 유용 할 수 있으며 컴파일러가 그렇게하는 것이 합리적입니다.
다음 인터럽트까지 방금 돌리고 싶은 경우는 드물지만 C로 쓰면 분명히 예상됩니다. (그리고 무엇 않고 , GCC와 연타에 일어나는 무한 루프는 래퍼 함수 내부에있을 때 연타 제외).
예를 들어, 원시 OS 커널에서 스케줄러에 실행할 태스크가 없으면 유휴 태스크를 실행할 수 있습니다. 그 첫 번째 구현은입니다 while(1);
.
또는 절전 유휴 기능이없는 하드웨어의 경우 이것이 유일한 구현 일 수 있습니다. (2000 년대 초까지는 x86에서는 드물지 않다고 생각했습니다.이 hlt
명령이 존재 하더라도 IDK는 CPU가 저전력 유휴 상태를 시작할 때까지 상당한 양의 전력을 절약했습니다.)