예, ISO C ++은 이러한 선택을 구현하기 위해 구현을 허용하지만 필수는 아닙니다.
또한 ISO C ++을 사용하면 프로그램에서 UB를 발견 한 경우 (예 : 오류를 찾는 데 도움이 됨) 의도적으로 충돌하는 코드 (예 : 잘못된 명령으로)를 생성 할 수 있습니다. (또는 DeathStation 9000이기 때문에. C ++ 구현이 실제 목적에 유용하기에는 엄격하게 준수하는 것만으로는 충분하지 않습니다). 따라서 ISO C ++을 사용하면 컴파일러는 초기화되지 않은을 읽는 비슷한 코드에서도 (다른 이유로) 충돌 한 asm을 만들 수 uint32_t
있습니다. 비록 트랩 표현이없는 고정 레이아웃 유형이어야합니다.
실제 구현이 어떻게 작동하는지에 대한 흥미로운 질문이지만 대답이 다르더라도 현대 C ++은 이식 가능한 버전의 어셈블리 언어가 아니기 때문에 코드가 여전히 안전하지 않다는 것을 기억하십시오.
x86-64 System V ABI를 컴파일하고 있는데 bool
, 레지스터의 함수 arg가 비트 패턴 false=0
과true=1
레지스터 1 의 하위 8 비트로 표시되도록 지정합니다 . 메모리에서 bool
1 바이트 유형은 다시 0 또는 1의 정수 값을 가져야합니다.
(ABI는 동일한 플랫폼의 컴파일러가 동의하는 일련의 구현 선택 사항이므로 유형 크기, 구조체 레이아웃 규칙 및 호출 규칙을 포함하여 서로의 기능을 호출하는 코드를 만들 수 있습니다.)
ISO C ++에서는이를 지정하지 않지만이 ABI 결정은 bool-> int 변환을 저렴하게 (단지 확장없이)하기 때문에 널리 퍼져 있습니다. 컴파일러가 bool
x86이 아닌 모든 아키텍처에 대해 0 또는 1을 가정하지 못하게하는 ABI는 알지 못합니다. 이것은 최적화 등 허용 !mybool
으로는 xor eax,1
: 낮은 비트 플립 단일 CPU 인스트럭션에 0과 1 사이의 비트 / 정수 / BOOL 플립 수있는 모든 가능한 코드 . 또는 유형 a&&b
에 대한 비트 AND로 컴파일 합니다 bool
. 일부 컴파일러는 실제로 컴파일러에서 부울 값을 8 비트로 사용합니다. 그들에 대한 작업이 비효율적입니까? .
일반적으로 as-if 규칙을 사용하면 컴파일러에서 컴파일 할 대상 플랫폼에서 사실 을 활용할 수 있습니다. 최종 결과는 C ++ 소스와 동일한 외부에서 볼 수있는 동작을 구현하는 실행 가능한 코드이기 때문입니다. (정의되지 않은 동작이 실제로 "외부 적으로 볼 수있는"항목에 적용되는 모든 제한 사항 : 디버거가 아니라 올바른 형식의 / 합법적 인 C ++ 프로그램의 다른 스레드에서 발생합니다.)
컴파일러는 코드 생성에서 ABI 보증을 최대한 활용하고 찾은 것과 같은 코드를 최적화 strlen(whichString)
합니다
5U - boolValue
. (BTW,이 최적화는 일종의 영리하지만 memcpy
즉각적인 데이터 저장소로 분기 및 인라인에 비해 근시안적 일 수 있습니다.)
또는 컴파일러가 포인터 테이블을 생성하고 bool
다시 0 또는 1이라고 가정 하고 정수 값으로 색인을 생성 할 수 있습니다 ( 이 가능성은 @Barmar의 답변이 제안한 것 입니다).
귀하의 __attribute((noinline))
최적화를 생성자로 사용할 스택에서 바이트를로드 단지 그 소리에지도 활성화 uninitializedBool
. 또한 상기 목적을위한 공간을 만들어 main
으로 push rax
(효율적인로서 대해 작고 다양한 이유로있는 sub rsp, 8
어떤 정도로 쓰레기가 항목을 AL에 있었다) main
가 사용되는 값이다 uninitializedBool
. 이것이 실제로 당신이 아닌 값을 얻은 이유 0
입니다.
5U - random garbage
부호없는 큰 값으로 쉽게 줄 바꿈하여 memcpy가 매핑되지 않은 메모리로 이동할 수 있습니다. 대상이 스택이 아닌 정적 저장소에 있으므로 반환 주소 나 무언가를 덮어 쓰지 않습니다.
다른 구현은 다른 선택을 할 수 있습니다 (예 : false=0
및) true=any non-zero value
. 그리고 아마 그 소리하는 코드를하지 것이라고에 대한 충돌 이 UB의 특정 인스턴스입니다. (하지만 원하는 경우 여전히 허용됩니다.) x86-64가 수행하는 다른 작업을 선택하는 구현에 대해서는 알지 bool
못하지만 C ++ 표준은 아무도하지 않거나 원하지 않는 많은 일을 허용합니다. 현재 CPU와 같은 하드웨어.
ISO C ++에서는의 객체 표현을 검사하거나 수정할 때 찾을 수있는 내용을 지정하지 않은 상태로 둡니다bool
. (예를 들어 ,에 별명을 지정할 수 memcpy
있으므로 bool
into unsigned char
를 사용하면 패딩 비트가 보장되지 않으므로 C ++ 표준은 공식적으로 UB없이 객체 표현을 16 진수 덤프 할 수 있습니다. 객체를 복사하기위한 포인터 캐스팅 표현은 물론 을 할당하는 것과 다르 므로 부울 화를 0 또는 1로 지정하지 않으면 원시 객체 표현을 얻을 수 있습니다.)char*
unsigned char
char foo = my_bool
당신은 한 부분적 으로 컴파일러에서이 실행 경로에 UB을 "숨겨진"noinline
. 인라인이 아니더라도 절차 간 최적화는 여전히 다른 함수의 정의에 의존하는 함수의 버전을 만들 수 있습니다. (먼저 clang은 기호 삽입이 발생할 수있는 Unix 공유 라이브러리가 아닌 실행 파일을 만들고 있습니다. 둘째, 정의 내부의 class{}
정의이므로 모든 번역 단위는 동일한 정의를 가져야합니다. inline
키워드 와 마찬가지로 )
따라서 실행 경로가 불가피하게 정의되지 않은 동작을 만나기 때문에 컴파일러는에 대한 정의로 ret
또는 ud2
(잘못된 명령)을 방출 할 수 있습니다. main
main
(인라인이 아닌 생성자를 통해 경로를 따르기로 결정한 경우 컴파일러가 컴파일 타임에 볼 수있는 동안)
UB를 만나는 모든 프로그램은 전체 존재에 대해 완전히 정의되지 않았습니다. 그러나 if()
실제로 실행되지 않는 함수 또는 분기 내부의 UB 는 나머지 프로그램을 손상시키지 않습니다. 실제로 이것은 ret
컴파일 타임에 UB를 포함하거나 이끌어 낼 수있는 전체 기본 블록에 대해 컴파일러가 잘못된 명령 또는을 방출하거나 아무것도 방출하지 않고 다음 블록 / 함수에 빠질 수 있음을 의미합니다.
실제로 GCC와 Clang은 실제로 는ud2
말이되지 않는 실행 경로에 대한 코드를 생성하는 대신 실제로 UB에서 방출 합니다. 또는 비 void
기능 종료에서 벗어나는 경우 gcc는 때때로 ret
명령어를 생략합니다 . "내 기능이 RAX에있는 쓰레기와 함께 반환 될 것"이라고 생각했다면, 잘못 알고 있습니다. 최신 C ++ 컴파일러는 언어를 더 이상 이식 가능한 어셈블리 언어처럼 취급하지 않습니다. 독립형 비 인라인 버전의 함수가 asm으로 보이는 방법에 대한 가정없이 프로그램이 실제로 유효한 C ++이어야합니다.
또 다른 재미있는 예는 AMD64에서 mmap'ed 메모리에 대한 정렬되지 않은 액세스가 때때로 segfault 인 이유 는 무엇입니까? . x86은 정렬되지 않은 정수에서 오류가 발생하지 않습니다. 왜 잘못 정렬 된 uint16_t*
것이 문제가 될까요? 왜냐하면 alignof(uint16_t) == 2
SSE2로 자동 벡터화 할 때이 가정을 위반하면 segfault가 발생하기 때문입니다.
clang 개발자의 기사 인 정의되지 않은 동작 # 1 / 3에 대해 모든 C 프로그래머가 알아야 할 사항 도 참조하십시오 .
요점 : 컴파일러가 컴파일 타임에 UB를 발견 한 경우 비트 패턴이 유효한 객체 표현 인 ABI를 대상으로하는 경우에도 UB를 발생시키는 코드를 통해 경로를 "파손"(놀람 한 asm을 방출 할 수 있음 ) 할 수bool
있습니다.
프로그래머가 많은 실수, 특히 현대 컴파일러가 경고하는 것들에 대한 적대감이 예상됩니다. 따라서 -Wall
경고를 사용 하고 수정 해야합니다 . C ++는 사용자에게 친숙한 언어가 아니며 컴파일하려는 대상에 asm으로 안전하더라도 C ++의 안전하지 않을 수 있습니다. (예를 들어, 부호있는 오버플로는 C ++에서 UB이며 컴파일러는을 사용하지 않으면 2의 보수 x86을 컴파일 할 때도 발생하지 않는다고 가정합니다 clang/gcc -fwrapv
.)
컴파일 타임에 보이는 UB는 항상 위험하며, 링크 타임 최적화를 통해 컴파일러에서 UB를 실제로 숨겨서 어떤 종류의 asm이 생성되는지에 대해 추론 할 수는 없습니다.
지나치게 과격하지 않아야합니다. 종종 컴파일러는 무언가를 피하고 UB 일 때도 예상대로 코드를 내 보냅니다. 그러나 컴파일러가 값 범위에 대해 더 많은 정보를 얻는 최적화를 구현하면 미래에 문제가 될 수 있습니다 (예 : 변수가 음수가 아닌 경우 x86에서 0 확장을 해제하도록 부호 확장을 최적화 할 수 있음) 64). 예를 들어, 현재 gcc 및 clang에서 수행 tmp = a+INT_MIN
은 a<0
항상 거짓으로 최적화되지 않으며 tmp
항상 음수입니다. ( 이 2의 보수 목표에서 INT_MIN
+ a=INT_MAX
는 음수 이므로 a
그보다 높을 수 없습니다.)
따라서 gcc / clang은 현재 계산 된 입력에 대한 범위 정보를 도출하기 위해 역 추적하지 않으며 부호있는 오버플로가 없다는 가정 ( 예 : Godbolt)을 기반으로 한 결과 에 대해서만 추적 합니다. 이것이 최적화인지 사용자 친화의 이름으로 의도적으로 "누락 된"것인지 또는 무엇인지 모르겠습니다.
또한 구현 (일명 컴파일러)은 ISO C ++가 정의되지 않은 상태로 동작을 정의 할 수 있습니다 . 예를 들어, Intel의 내장 함수 ( _mm_add_ps(__m128, __m128)
수동 SIMD 벡터화 와 같은) 를 지원하는 모든 컴파일러는 잘못 정렬 된 포인터를 형성 할 수 있어야합니다.이 포인터는 역 참조 하지 않아도 C ++에서 UB입니다 . __m128i _mm_loadu_si128(const __m128i *)
잘못 정렬 복용하여 정렬되지 않은로드를 수행 __m128i*
하지 않는, 인수를 void*
하거나 char*
. 하드웨어 벡터 포인터와 해당 유형 사이의`reinterpret_cast`는 정의되지 않은 동작입니까?
GNU C / C ++는 -fwrapv
일반적인 부호있는 오버플로 UB 규칙과 별도로 음의 부호있는 숫자 (조차없는 ) 를 왼쪽으로 이동시키는 동작을 정의합니다 . ( 이것은 ISO C ++에서 UB 이며 부호있는 숫자의 오른쪽 시프트는 구현 정의 (논리 대 산술)입니다. 좋은 품질의 구현은 HW에서 산술 오른쪽 시프트가있는 산술을 선택하지만 ISO C ++은 지정하지 않습니다). 이 내용은 G 표준 매뉴얼의 정수 섹션에 문서화되어 있으며 C 표준은 구현 방식에 따라 구현 방법이 필요합니다.
컴파일러 개발자들이 염려하는 구현 품질 문제는 분명히 있습니다. 그들은 일반적으로 의도적으로 적대적인 컴파일러를 만들 려고 시도 하지 않지만 C ++의 모든 UB 움푹 들어간 곳 (정의 된 것을 제외하고)을 사용하여 더 잘 최적화하는 것은 때때로 거의 구별 할 수 없습니다.
각주 1 : 상위 56 비트는 일반적으로 레지스터보다 좁은 유형의 경우 수신자가 무시해야하는 가비지 일 수 있습니다.
( 다른 ABI 는 여기에서 다른 선택을합니다 . 일부는 MIPS64 및 PowerPC64와 같이 함수로 전달되거나 함수에서 반환 될 때 레지스터를 채우기 위해 좁은 정수 유형을 0 또는 부호 확장해야합니다. 이 x86-64 답변 의 마지막 섹션을 참조하십시오. 이전 ISA와 비교 한 것 입니다.)
예를 들어, 호출자는 a & 0x01010101
호출하기 전에 RDI에서 계산 하여 다른 용도로 사용 했을 수 있습니다 bool_func(a&1)
. 호출자는의 &1
일부로 이미 하위 바이트를 수행했기 때문에 최적화 할 수 and edi, 0x01010101
있으며, 수신자가 상위 바이트를 무시해야한다는 것을 알고 있습니다.
또는 부울이 3 번째 인수로 전달되면 코드 크기를 최적화하는 호출자가 mov dl, [mem]
대신 대신 로드하여 movzx edx, [mem]
RDX의 이전 값 (또는 다른 부분 레지스터 효과에 따라 잘못된 의존성으로 1 바이트를 절약 할 수 있음) CPU 모델). 또는 어쨌든 REX 접두사가 필요하기 때문에 , mov dil, byte [r10]
대신 첫 번째 인수에 대해 movzx edi, byte [r10]
.
이것은 왜 그 소리를 방출이다 movzx eax, dil
에서 Serialize
대신, sub eax, edi
. 정수 인수의 경우 clang은 문서화되지 않은 gcc 및 clang의 동작에 따라 좁은 정수를 32 비트로 0 또는 부호 확장하거나 32 비트 오프셋을 포인터에 추가 할 때 부호 또는 0 확장이 필요합니다. x86-64 ABI?
그래서 나는 그것이 같은 일을하지 않는다는 것을 알고 싶어했습니다 bool
.)
각주 2 : 분기 후에는 4 바이트 mov
즉석 또는 4 바이트 + 1 바이트 저장소 만 있으면 됩니다. 길이는 상점 너비 + 오프셋에 내재되어 있습니다.
OTOH, glibc memcpy는 길이에 따라 겹치는 두 개의 4 바이트로드 / 스토어를 수행하므로 실제로는 부울의 모든 조건부 분기가 없어집니다. glibc의 memcpy / memmove에 있는 L(between_4_7):
블록 을 보십시오 . 또는 적어도 memcpy의 boolean에서 청크 크기를 선택하는 것과 동일한 방식으로 진행하십시오.
인라인 인 경우 2x mov
-immediate + cmov
와 조건부 오프셋을 사용하거나 문자열 데이터를 메모리에 남겨 둘 수 있습니다.
또는 Intel Ice Lake ( Fast Short REP MOV 기능 사용 )를 튜닝하는 경우 실제 성능 rep movsb
이 최적 일 수 있습니다. glibc memcpy
는 rep movsb
이 기능을 갖춘 CPU에서 작은 크기로 사용 하기 시작하여 많은 분기를 절약 할 수 있습니다.
UB 감지 및 초기화되지 않은 값 사용을위한 도구
gcc 및 clang에서는 -fsanitize=undefined
런타임에 발생하는 UB에서 경고 또는 오류가 발생하는 런타임 계측을 추가하기 위해 컴파일 할 수 있습니다 . 그러나 그것은 단일화 된 변수를 잡을 수 없습니다. "초기화되지 않은"비트를위한 공간을 만들기 위해 유형 크기를 늘리지 않기 때문입니다.
https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/을 참조 하십시오
초기화되지 않은 데이터의 사용법을 찾으려면 clang / LLVM에 Address Sanitizer 및 Memory Sanitizer가 있습니다. https://github.com/google/sanitizers/wiki/MemorySanitizer 는 clang -fsanitize=memory -fPIE -pie
초기화되지 않은 메모리 읽기 를 감지하는 예를 보여줍니다 . 최적화 없이 컴파일하면 가장 잘 작동 할 수 있으므로 모든 변수 읽기는 실제로 asm의 메모리에서로드됩니다. -O2
부하가 최적화되지 않는 경우 에 사용됨을 나타 냅니다. 나는 그것을 직접 시도하지 않았다. (어떤 경우, 배열을 합치기 전에 누산기를 초기화하지 않으면 clang -O3는 초기화되지 않은 벡터 레지스터에 합산되는 코드를 생성합니다. 따라서 최적화를 통해 UB와 관련된 메모리 읽기가없는 경우가 있습니다 하지만-fsanitize=memory
생성 된 asm을 변경하면이를 확인할 수 있습니다.)
초기화되지 않은 메모리의 복사와 간단한 논리 및 산술 연산을 허용합니다. 일반적으로 MemorySanitizer는 메모리에 초기화되지 않은 데이터의 확산을 자동으로 추적하고 초기화되지 않은 값에 따라 코드 분기를 수행하거나 수행하지 않을 때 경고를보고합니다.
MemorySanitizer는 Valgrind (Memcheck 도구)에있는 일부 기능을 구현합니다.
호출이 glibc가 있기 때문에 그것은이 경우에 작동합니다 memcpy
으로 length
초기화되지 않은 메모리 계산이 지점에서 결과 (도서관 내부)에 기반합니다 length
. 방금 cmov
, 인덱싱 및 두 개의 저장소를 사용한 완전히 분기없는 버전을 인라인 한 경우 작동하지 않았을 수 있습니다.
Valgrindmemcheck
는 또한 이런 종류의 문제를 찾아서 프로그램이 초기화되지 않은 데이터를 단순히 복사하는지에 대해 불평하지 않습니다. 그러나 그것은 초기화되지 않은 데이터에 의존하는 외부에서 보이는 행동을 포착하기 위해 "조건부 점프 또는 이동이 초기화되지 않은 값에 의존하는 경우"를 감지 할 것이라고 말합니다.
아마도로드에 플래그를 지정하지 않는 배후의 아이디어는 구조체에 패딩이있을 수 있으며 개별 구조체가 한 번에 하나씩 만 작성된 경우에도 전체 벡터로드 / 스토어로 전체 구조체 (패딩 포함)를 복사하는 것은 오류가 아니라는 것입니다. asm 레벨에서 패딩 된 내용과 실제로 값의 일부에 대한 정보가 손실되었습니다.