"휘발성"의 정의가 이처럼 변동성이 있습니까? 아니면 GCC에 표준 준수 문제가 있습니까?


89

(WinAPI의 SecureZeroMemory와 같은) 항상 메모리를 0으로 만들고 컴파일러가 그 후에는 메모리에 다시 액세스하지 않을 것이라고 생각하더라도 최적화되지 않는 함수가 필요합니다. 휘발성에 대한 완벽한 후보처럼 보입니다. 하지만 실제로 GCC와 함께 작동하는 데 몇 가지 문제가 있습니다. 다음은 함수의 예입니다.

void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

충분히 간단합니다. 그러나 GCC가 호출하면 실제로 생성하는 코드는 컴파일러 버전과 실제로 제로화하려는 바이트의 양에 따라 크게 다릅니다. https://godbolt.org/g/cMaQm2

  • GCC 4.4.7 및 4.5.3은 휘발성을 무시하지 않습니다.
  • GCC 4.6.4 및 4.7.3은 어레이 크기 1, 2 및 4의 휘발성을 무시합니다.
  • 4.9.2까지 GCC 4.8.1은 어레이 크기 1과 2의 휘발성을 무시합니다.
  • 5.3까지 GCC 5.1은 어레이 크기 1, 2, 4, 8에 대해 휘발성을 무시합니다.
  • GCC 6.1은 모든 배열 크기에 대해 무시합니다 (일관성을위한 보너스 포인트).

내가 테스트 한 다른 컴파일러 (clang, icc, vc)는 모든 컴파일러 버전 및 배열 크기로 예상 할 수있는 저장소를 생성합니다. 그래서이 시점에서 나는 이것이 (아주 오래되고 심각한가?) GCC 컴파일러 버그인지, 아니면 이것이 실제로 동작을 준수한다는 것을 부정확하게 표준에서 휘발성의 정의로, 본질적으로 포터블을 작성하는 것을 불가능하게 만드는지 궁금합니다. " SecureZeroMemory "기능?

편집 : 몇 가지 흥미로운 관찰.

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <atomic>

void callMeMaybe(char* buf);

void volatileZeroMemory(volatile void* ptr, std::size_t size)
{
    for (auto bytePtr = static_cast<volatile std::uint8_t*>(ptr); size-- > 0; )
    {
        *bytePtr++ = 0;
    }

    //std::atomic_thread_fence(std::memory_order_release);
}

std::size_t foo()
{
    char arr[8];
    callMeMaybe(arr);
    volatileZeroMemory(arr, sizeof arr);
    return sizeof arr;
}

callMeMaybe ()에서 가능한 쓰기는 6.1을 제외한 모든 GCC 버전이 예상 저장소를 생성하도록합니다. 메모리 펜스에 주석을 달면 GCC 6.1이 저장소를 생성 할 수 있지만 callMeMaybe ()에서 가능한 쓰기와 만 결합됩니다.

누군가는 또한 캐시를 플러시하도록 제안했습니다. Microsoft는 "SecureZeroMemory"에서 캐시를 전혀 플러시 하지 않습니다 . 어쨌든 캐시는 매우 빠르게 무효화 될 가능성이 높으므로 이것은 큰 문제가 아닐 것입니다. 또한 다른 프로그램이 데이터를 조사하려고하거나 페이지 파일에 기록 될 경우 항상 0으로 된 버전이됩니다.

독립형 함수에서 memset ()을 사용하는 GCC 6.1에 대한 몇 가지 우려 사항도 있습니다. Godbolt의 GCC 6.1 컴파일러는 GCC 6.1이 일부 사람들의 독립형 기능에 대해 일반 루프를 생성하는 것처럼 보이기 때문에 빌드가 손상 될 수 있습니다. (zwol의 답변에 대한 의견을 읽으십시오.)


4
IMHO 사용 volatile은 달리 입증되지 않는 한 버그입니다. 하지만 버그 일 가능성이 높습니다. volatile위험 할 정도로 너무 불명확합니다. 사용하지 마십시오.
Jesper Juhl

19
@JesperJuhl : 아니요, volatile이 경우에는 적절합니다.
Dietrich Epp

9
@NathanOliver : 컴파일러는 memset. 문제는 컴파일러가 정확히 무엇을하는지 알고 있다는 것 memset입니다.
Dietrich Epp

8
@PaulStelian은 : a를 것 그 volatile포인터를, 우리는 포인터를 원하는 volatile(우리가 여부를 상관하지 않습니다 ++엄격하지만 여부를 *p = 0엄격하다).
Dietrich Epp

7
@JesperJuhl : 휘발성에 대해 과소 지정된 것은 없습니다.
GManNickG

답변:


82

GCC의 동작 일치 할 수 있으며 그렇지 않더라도 volatile이와 같은 경우 원하는 작업을 수행하는 데 의존해서는 안됩니다 . C위원회 volatile는 메모리 매핑 된 하드웨어 레지스터 및 비정상적인 제어 흐름 중에 수정 된 변수 (예 : 신호 처리기 및 setjmp)를 위해 설계되었습니다 . 그것들이 신뢰할 수있는 유일한 것들입니다. 일반적인 "이를 최적화하지 마십시오"주석으로 사용하는 것은 안전하지 않습니다.

특히 기준은 요점에 대해서는 불분명하다. (귀하의 코드를 C로 변환 했습니다. 여기서 C와 C ++ 사이에 차이 가 있어서는 안됩니다 . 또한 의심스러운 최적화 전에 발생할 인라인을 수동으로 수행하여 컴파일러가 그 시점에서 "보는"것을 보여줍니다. .)

extern void use_arr(void *, size_t);
void foo(void)
{
    char arr[8];
    use_arr(arr, sizeof arr);

    for (volatile char *p = (volatile char *)arr;
         p < (volatile char *)(arr + 8);
         p++)
      *p = 0;
}

메모리 지우기 루프는 arr휘발성으로 한정된 lvalue를 통해 액세스 하지만 arr그 자체는 선언 되지 않습니다volatile . 따라서 적어도 C 컴파일러가 루프에 의해 만들어진 저장소가 "죽었다"고 추론하고 루프를 완전히 삭제하는 것은 틀림없이 허용됩니다. C Rationale에는위원회 가 해당 상점을 보존하도록 요구하는 것을 의미하는 텍스트가 있지만, 내가 읽은대로 표준 자체는 실제로 해당 요구 사항을 만들지 않습니다.

표준이 무엇을 요구하거나 요구하지 않는지에 대한 자세한 내용은 휘발성 로컬 변수가 휘발성 인수와 다르게 최적화되는 이유와 옵티마이 저가 후자에서 no-op 루프를 생성하는 이유를 참조하십시오. , 휘발성 참조 / 포인터를 통해 선언 된 비 휘발성 객체에 액세스하면 해당 액세스에 휘발성 규칙이 부여됩니까? , GCC 버그 71793 .

위원회가 생각한 내용에 대한 자세한 내용 volatileC99 Rationale 에서 "volatile"이라는 단어를 검색하십시오 . John Regehr의 논문 " Volatiles are Miscompiled " 프로그래머의 기대치를 volatile프로덕션 컴파일러가 어떻게 충족하지 못할 수 있는지 자세히 설명합니다 . LLVM 팀의 에세이 시리즈 " 모든 C 프로그래머가 정의되지 않은 동작에 대해 알아야 할 사항 "은 구체적으로 volatile다루지는 않지만 최신 C 컴파일러가 "휴대용 어셈블러" 가 아닌 방법과 이유를 이해하는 데 도움이됩니다 .


원하는 작업 을 수행 하는 함수를 구현하는 방법에 대한 실제적인 질문 volatileZeroMemory: 표준이 요구하거나 요구하는 것이 무엇이든 상관없이이를 volatile위해 사용할 수 없다고 가정하는 것이 가장 현명 할 것 입니다. 거기에 있다 가 작동하지 않았다 경우가 너무 많은 다른 물건을 깰 때문에 작업에 의존 할 수있는 대안은 :

extern void memory_optimization_fence(void *ptr, size_t size);
inline void
explicit_bzero(void *ptr, size_t size)
{
   memset(ptr, 0, size);
   memory_optimization_fence(ptr, size);
}

/* in a separate source file */
void memory_optimization_fence(void *unused1, size_t unused2) {}

그러나 memory_optimization_fence어떤 상황에서도 인라인되지 않도록해야합니다 . 자체 소스 파일에 있어야하며 링크 시간 최적화의 대상이되어서는 안됩니다.

컴파일러 확장에 의존하는 다른 옵션이 있으며 일부 상황에서 사용할 수 있고 더 엄격한 코드를 생성 할 수 있지만 (이 중 하나는이 답변의 이전 버전에 표시됨) 보편적 인 옵션은 없습니다.

(2 explicit_bzero개 이상의 C 라이브러리에서 해당 이름으로 사용할 수 있으므로 함수를 호출하는 것이 좋습니다. 이름에 대해 적어도 4 개의 다른 경쟁자가 있지만 각각 하나의 C 라이브러리에서만 채택되었습니다.)

이 작업을 수행 할 수 있더라도 충분하지 않을 수도 있다는 점도 알아야합니다. 특히

struct aes_expanded_key { __uint128_t rndk[16]; };

void encrypt(const char *key, const char *iv,
             const char *in, char *out, size_t size)
{
    aes_expanded_key ek;
    expand_key(key, ek);
    encrypt_with_ek(ek, iv, in, out, size);
    explicit_bzero(&ek, sizeof ek);
}

AES 가속 명령어가있는 하드웨어를 가정 expand_key하고 encrypt_with_ek인라인 인 경우 컴파일러는를 ek호출 할 때까지 벡터 레지스터 파일에 전적으로 보관할 수 있습니다.이 파일 explicit_bzero을 사용 하면 민감한 데이터를 스택에 복사하여 데이터 를 지울 수 있습니다. 더 나쁜 것은 벡터 레지스터에 여전히 앉아있는 키에 대해 어리석은 짓을하지 마십시오!


6
흥미 롭군요 ...위원회의 의견에 대한 언급을보고 싶습니다.
Dietrich Epp

10
어떻게의 6.7.3 (7)이 사각의 정의 volatile로서 5.1.2.3에 기재된 바와 같이 [...] 따라서, 이러한 객체에 모든 참조 표현에서, 추상 시스템의 규칙에 따라 엄격하게 평가된다. 또한 모든 시퀀스 포인트에서 객체에 마지막으로 저장된 값 은 이전에 언급 된 알려지지 않은 요인에 의해 수정 된 경우를 제외하고 추상 기계 에서 규정 한 값과 일치해야합니다 . 휘발성 한정 유형을 가진 객체에 대한 액세스를 구성하는 것은 구현 정의입니다. ?
Iwillnotexist Idonotexist

15
@IwillnotexistIdonotexist 그 구절의 핵심 단어는 객체 입니다. volatile sig_atomic_t flag;휘발성 객체 입니다. 휘발성으로 한정된 lvalue를 통한 액세스*(volatile char *)foo 일 뿐이며 표준은 특수 효과를 가질 필요가 없습니다.
zwol

3
표준은 "준수"구현을 위해 어떤 기준을 충족해야하는지 알려줍니다. 주어진 플랫폼에서 구현이 "좋은"구현 또는 "사용 가능한"구현이되기 위해 충족해야하는 기준을 설명하려고 노력하지 않습니다. GCC의 처리는 volatile이를 "준수"구현으로 만들기에 충분할 수 있지만 이것이 "좋은"또는 "유용한"것으로 충분하다는 것을 의미하지는 않습니다. 많은 종류의 시스템 프로그래밍의 경우 이러한 점에서 비참하게 부족한 것으로 간주되어야합니다.
supercat

3
C 스펙은 또한 오히려 직접적으로 "그 값이 사용되지 않고 필요한 부작용이 생성되지 않는다는 것을 추론 할 수있는 경우 표현식의 일부를 평가할 필요가 없습니다 ( 함수 호출 또는 휘발성 객체 액세스로 인한 결과 포함 ). . " (내 것을 강조).
Johannes Schaub-litb

15

(WinAPI의 SecureZeroMemory와 같은) 항상 메모리를 0으로 만들고 최적화되지 않는 함수가 필요합니다.

이것이 바로 표준 기능 memset_s입니다.


휘발성이 동작 준수 여부에 관해서는, 그 말을 열심히 비트, 그리고 휘발성이되었습니다 말했다 긴 버그 퍼진 것으로.

한 가지 문제는 사양에 "휘발성 객체에 대한 액세스는 추상 기계의 규칙에 따라 엄격하게 평가됩니다"라고 명시되어 있다는 것입니다. 그러나 그것은 휘발성이 추가 된 포인터를 통해 비 휘발성 객체에 액세스하는 것이 아니라 '휘발성 객체'만을 의미합니다. 따라서 컴파일러가 실제로 휘발성 객체에 액세스하고 있지 않다고 말할 수 있다면 결국 객체를 휘발성으로 취급 할 필요가 없습니다.


4
참고 : 이것은 C11 표준의 일부이며 아직 모든 도구 모음에서 사용할 수있는 것은 아닙니다.
Dietrich Epp

5
흥미롭게도이 함수는 C11 용으로 표준화되었지만 C ++ 11, C ++ 14 또는 C ++ 17 용은 아닙니다. 기술적으로는 C ++에 대한 해결책이 아니지만 실용적인 관점에서 보면 이것이 최선의 선택이라고 동의합니다. 이 시점에서 GCC의 동작이 준수하는지 여부가 궁금합니다. 편집 : 실제로 VS 2015에는 memset_s가 없으므로 아직 이식성이 없습니다.
cooky451

2
@ cooky451 나는 C ++ 17이 참조로 C11 표준 라이브러리를 끌어 온다고 생각 했습니다 (두 번째 Misc 참조).
NWP

14
또한 memset_sC11 표준으로 설명하는 것은 과장된 표현입니다. Annex K의 일부이며 C11에서는 선택 사항이므로 C ++에서도 선택 사항입니다. 기본적으로 마이크로 소프트를 포함한 모든 구현 자 들은 그 아이디어가 처음 (!)에 들어간 것을 거부했습니다. 마지막으로 그들이 C-next에서 그것을 폐기하는 것에 대해 이야기한다고 들었습니다.
zwol

8
@ cooky451 특정 서클에서 Microsoft는 기본적으로 다른 모든 사람의 반대에 대해 C 표준에 항목을 강제 적용한 다음 스스로 구현하지 않는 것으로 유명합니다. (이것의 가장 심각한 예는 기본 유형 size_t이 허용 되는 규칙을 C99에서 완화 한 것입니다. Win64 ABI는 C90을 준수하지 않습니다. 그럴 것입니다 ... 괜찮지 않지만 끔찍하지는 않습니다 ... if MSVC 실제로 같은 C99 물건을 집어했다 uintmax_t%zu적시에,하지만 그들은 하지 않았다 ).
zwol

2

이 버전을 이식 가능한 C ++로 제공합니다 (의미론은 미묘하게 다르지만).

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size];

    while (size--)
    {
        *bytePtr++ = 0;
    }
}

이제 객체의 휘발성보기를 통해 만든 비 휘발성 객체에 대한 액세스뿐 아니라 휘발성 객체 에 대한 쓰기 액세스 권한이 있습니다 .

의미 론적 차이는 메모리가 재사용 되었기 때문에 메모리 영역을 점유 한 모든 객체의 수명이 이제 공식적으로 종료된다는 것입니다. 따라서 내용을 제로화 한 후 객체에 대한 액세스는 이제 확실히 정의되지 않은 동작입니다 (이전에는 대부분의 경우 정의되지 않은 동작 이었지만 일부 예외는 확실히 존재했습니다).

마지막이 아닌 개체의 수명 동안이 제로화를 사용하려면 호출자는 배치 new를 사용 하여 원래 유형의 새 인스턴스를 다시 배치해야 합니다.

값 초기화를 사용하여 코드를 더 짧게 만들 수 있습니다.

void volatileZeroMemory(volatile void* const ptr, unsigned long long size)
{
    new (ptr) volatile unsigned char[size] ();
}

그리고이 시점에서 그것은 한 줄짜리이며 도우미 기능을 거의 보장하지 않습니다.


2
함수가 실행 된 후 객체에 대한 액세스가 UB를 호출하는 경우, 이는 이러한 액세스가 "삭제"되기 전에 객체가 보유한 값을 산출 할 수 있음을 의미합니다. 보안의 반대가 아닌 이유는 무엇입니까?
supercat

0

오른쪽에 휘발성 객체를 사용하고 컴파일러가 저장소를 배열에 보존하도록 강제하여 함수의 이식 가능한 버전을 작성할 수 있어야합니다.

void volatileZeroMemory(void* ptr, unsigned long long size)
{
    volatile unsigned char zero = 0;
    unsigned char* bytePtr = static_cast<unsigned char*>(ptr);

    while (size--)
    {
        *bytePtr++ = zero;
    }

    zero = static_cast<unsigned char*>(ptr)[zero];
}

zero개체를 선언 volatile보장하지만 컴파일러는 항상 0으로 평가에도 불구하고 그 가치에 대해 어떠한 가정도하지 않습니다 수있다.

최종 할당 식은 배열의 휘발성 인덱스에서 읽고 값을 휘발성 개체에 저장합니다. 이 읽기는 최적화 할 수 없으므로 컴파일러가 루프에 지정된 저장소를 생성해야합니다.


1
이것은 전혀 작동하지 않습니다 ... 생성되는 코드를보십시오.
cooky451

1
내 생성 된 ASM mo '를 더 잘 읽은 것은 함수 호출을 인라인하고 루핑을 유지하는 것처럼 보이지만 *ptr해당 루프 동안 저장하지 않거나 실제로 는 전혀 ... 루핑 만합니다. 저기, 내 머리가 간다.
underscore_d

3
@underscore_d 휘발성의 읽기를 보존하면서 저장소를 최적화하기 때문입니다.
D Krueger

1
그래, 그리고 그것은 변하지 않는 결과를 덤프한다 edx: 나는 이것을 얻는다 :.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
underscore_d

1
임의의 volatile unsigned char const채우기 바이트를 전달할 수 있도록 함수를 변경하면 ... 읽지 않습니다 . 생성 된 인라인 호출 volatileFill()[load RAX with sizeof] .L9: subq $1, %rax; jne .L9. 옵티 마이저 (A)가 채우기 바이트를 다시 읽지 않고 (B) 아무 작업도하지 않는 루프를 보존하는 이유는 무엇입니까?
underscore_d
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.