programing

잘못 정렬된 포인터를 유지하는 것이 잘 정의되어 있습니까? 포인터를 참조 취소하지 않는 한?

subpage 2023. 7. 23. 14:16
반응형

잘못 정렬된 포인터를 유지하는 것이 잘 정의되어 있습니까? 포인터를 참조 취소하지 않는 한?

저는 네트워크에서 들어오는 패킹/미첨가 이진 데이터를 구문 분석하는 C 코드를 가지고 있습니다.

이 코드는 Intel/x86에서 정상적으로 작동하지만 ARM에서 컴파일하면 자주 충돌합니다.

당신이 짐작했겠지만, 그 원인은 정렬되지 않은 포인터들이었습니다. 특히, 구문 분석 코드는 다음과 같은 의심스러운 일을 할 것입니다.

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]);  // misaligned access -- can crash under ARM!

그것은 분명히 ARM-land에서 날지 않을 것이기 때문에, 저는 이것을 좀 더 다음과 같이 보이도록 수정했습니다.

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord));  // slower but ARM-safe

(언어 변호사의 관점에서) 제 질문은: 저의 "ARM 고정" 접근법이 C 언어 규칙에서 잘 정의되어 있는가 하는 것입니다.

제 걱정은 단순히 잘못 정렬된 int32_t-pointer만 있어도 정의되지 않은 동작을 호출하기에 충분할 수 있다는 것입니다. (만약 제 우려가 유효하다면, 변경을 통해 문제를 해결할 수 있을 것 같습니다.)pNextWord의 유형(const int32_t *)로.(const char *)하지만 실제로 그렇게 할 필요가 없는 한 그렇게 하지 않는 게 낫습니다, 왜냐하면 그것은 손으로 포인터 스트라이드 산술을 하는 것을 의미하기 때문입니다.)

아니요, 새 코드에 정의되지 않은 동작이 있습니다.C116.3.2.3p7:

  1. 개체 유형에 대한 포인터는 다른 개체 유형에 대한 포인터로 변환될 수 있습니다.결과 포인터가 참조된 유형에 대해 올바르게 정렬되지 않은 경우 동작은 정의되지 않습니다. [...]

포인터 참조 해제에 대한 내용은 없습니다. 변환에도 정의되지 않은 동작이 있습니다.


실제로, 당신이 ARM-safe라고 가정하는 수정된 코드는 Intel-safe가 아닐 수 있습니다.컴파일러는 정렬되지 않은 액세스에서 충돌할 수 있는 Intel용 코드를 생성하는 것으로 알려져 있습니다.링크된 경우는 아니지만, 영리한 컴파일러가 주소가 실제로 정렬되어 있다는 증거로 변환을 받아들이고 다음을 위한 특수 코드를 사용할 수 있습니다.memcpy.


정렬은 차치하고, 첫 번째 발췌문도 엄격한 앨리어싱 위반을 겪습니다.C116.5p7:

  1. 객체는 다음 유형 중 하나를 가진 값 표현식을 통해서만 저장된 값에 액세스할 수 있습니다.88)
    • 개체의 유효 유형과 호환되는 유형,
    • 개체의 유효한 형식과 호환되는 형식의 정규 버전
    • 객체의 유효 유형에 해당하는 서명 또는 미서명 유형인 유형,
    • 객체의 유효 형식의 한정된 버전에 해당하는 서명 또는 서명되지 않은 형식인 형식,
    • 조합원들 사이에서 전술한 유형 중 하나를 포함하는 집합체 또는 조합 유형(재귀적으로 하위 집합체 또는 포함된 조합의 구성원 포함), 또는
    • 인물 활자

열배 이후 이후 이후로.buf[2048]정적으로 입력되며 각 요소는char따라서 요소의 효과적인 유형은 다음과 같습니다.char배열의 내용은 문자로만 액세스할 수 있습니다.int32_t

즉, 심지어

int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);

정의되지 않은 동작이 있습니다.

컴파일러/플랫폼 간에 멀티바이트 정수를 안전하게 구문 분석하려면 각 바이트를 추출하고 엔디언에 따라 정수로 조립할 수 있습니다.예를 들어 빅엔디안 버퍼에서 4바이트 정수를 읽으려면:

uint8_t* buf = any address;

uint32_t val = 0;
uint32_t  b0 = buf[0];
uint32_t  b1 = buf[1];
uint32_t  b2 = buf[2];
uint32_t  b3 = buf[3];

val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;

일부 컴파일러는 어떤 포인터도 해당 유형에 대해 적절하게 정렬되지 않은 값을 보유하지 않을 것이라고 가정하고 이에 의존하는 최적화를 수행할 수 있습니다.간단한 예로 다음을 고려합니다.

void copy_uint32(uint32_t *dest, uint32_t *src)
{
  memcpy(dest, src, sizeof (uint32_t));
}

둘 다인 경우dest그리고.src32비트 정렬 주소를 보유하며, 위의 기능은 정렬되지 않은 액세스를 지원하지 않는 플랫폼에서도 하나의 로드와 하나의 저장소로 최적화될 수 있습니다.가 형식 인수를 void*그러나 이러한 최적화는 정렬되지 않은 32비트 액세스가 일련의 바이트 액세스, 시프트 및 비트 단위 작업과 다르게 동작하는 플랫폼에서는 허용되지 않습니다.

Antti Haapala의 답변에서 언급했듯이, 결과 포인터가 제대로 정렬되지 않았을 때 포인터를 다른 유형으로 변환하는 것은 C 표준의 섹션 6.3.2.3p7에 따라 정의되지 않은 동작을 호출합니다.

는 수된코다사음용니다합만는드만 합니다.pNextWord에게 전해지다memcpy그것이 그것으로 변환되는 곳.void *그래서 당신은 심지어 유형의 변수도 필요하지 않습니다.uint32_t *읽기를 원하는 버퍼의 첫 번째 바이트 주소를 에 전달하기만 하면 됩니다.memcpy그러면 정렬에 대해 전혀 걱정할 필요가 없습니다.

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));

언급URL : https://stackoverflow.com/questions/51203570/is-it-well-defined-to-hold-a-misaligned-pointer-as-long-as-you-dont-ever-deref

반응형