C-두 포인터 간의 변환 동작
업데이트 2020-12-11 : 댓글에 제안 해 주신 @ "Some programmer dude"에게 감사드립니다. 내 근본적인 문제는 우리 팀이 동적 유형 스토리지 엔진을 구현하고 있다는 것입니다. 동적 데이터 유형을 저장하기 위해 16 정렬 된 여러 char array [PAGE_SIZE] 버퍼를 할당 합니다 (고정 된 구조체는 없음). 효율성을 위해 바이트 인코딩을 수행하거나 사용할 추가 공간을 할당 할 수 없습니다 memcpy
.
정렬이 결정되었으므로 (즉, 16) 나머지는 지정된 유형의 객체에 액세스하기 위해 포인터의 캐스트를 사용하는 것입니다. 예를 들면 다음과 같습니다.
int main() {
// simulate our 16-aligned malloc
_Alignas(16) char buf[4096];
// store some dynamic data:
*((unsigned long *) buf) = 0xff07;
*(((double *) buf) + 2) = 1.618;
}
그러나 우리 팀은이 작업이 정의되지 않은 동작인지 여부에 대해 이의를 제기합니다.
나는 다음과 같은 많은 유사한 질문을 읽었습니다.
- -Wcast-align이 x86에서 char *에서 int * 로의 캐스트에 대해 경고하지 않는 이유는 무엇입니까?
- 정렬되지 않은 위치에서 char 배열을 int로 캐스팅하는 방법은 무엇입니까?
- C 정의되지 않은 동작. 엄격한 앨리어싱 규칙 또는 잘못된 정렬?
- SEI CERT C CS EXP36-C
그러나 이것들은 C 표준에 대한 나의 해석과 다르기 때문에 그것이 나의 오해인지 알고 싶습니다.
주요 혼란은 C11 의 섹션 6.3.2.3 # 7에 관한 것입니다.
개체 유형에 대한 포인터는 다른 개체 유형에 대한 포인터로 변환 될 수 있습니다. 경우] 얻어진 포인터를 정확히 참조 타입) 68을 정렬하지 않고, 동작이 정의되지 않는다.
68) 일반적으로``올바르게 정렬 된 ''개념은 전 이적입니다. 유형 A에 대한 포인터가 유형 B에 대한 포인터에 대해 올바르게 정렬되고, 차례로 C 유형에 대한 포인터에 대해 올바르게 정렬되면 유형에 대한 포인터 A는 C 유형에 대한 포인터에 대해 올바르게 정렬됩니다.
않는 결과 포인터는 여기를 참조 포인터 개체 또는 포인터 값 ?
제 생각에는 대답이 Pointer Object 라고 생각 하지만 더 많은 대답이 Pointer Value 를 나타내는 것 같습니다 .
해석 A : 포인터 객체
내 생각은 다음과 같습니다. 포인터 자체는 객체입니다. 6.2.5 # 28 에 따르면 다른 포인터는 다른 표현 및 정렬 요구 사항을 가질 수 있습니다. 따라서 6.3.2.3 # 7 에 따르면 두 포인터가 동일한 정렬을 갖는 한 정의되지 않은 동작없이 안전하게 변환 될 수 있지만 역 참조 될 수 있다는 보장은 없습니다. 프로그램에서이 아이디어를 표현하십시오.
#include <stdio.h>
int main() {
char buf[4096];
char *pc = buf;
if (_Alignof(char *) == _Alignof(int *)) {
// cast safely, because they have the same alignment requirement?
int *pi = (int *) pc;
printf("pi: %p\n", pi);
} else {
printf("char * and int * don't have the same alignment.\n");
}
}
해석 B : 포인터 값
그러나 C11 표준이 Pointer Object 대신 참조 된 유형 에 대한 Pointer Value 에 대해 이야기 하는 경우 . 위 코드의 정렬 확인은 의미가 없습니다. 프로그램에서이 아이디어를 표현하십시오.
#include <stdio.h>
int main() {
char buf[4096];
char *pc = buf;
/*
* undefined behavior, because:
* align of char is 1
* align of int is 4
*
* and we don't know whether the `value` of pc is 4-aligned.
*/
int *pi = (int *) pc;
printf("pi: %p\n", pi);
}
어떤 해석이 맞습니까?
답변
해석 B가 맞습니다. 표준은 객체 자체가 아니라 객체에 대한 포인터에 대해 말하는 것입니다. "결과 포인터"는 캐스트의 결과를 참조하고 캐스트는 lvalue를 생성하지 않으므로 캐스트 이후의 포인터 값을 참조합니다.
예제의 코드를 사용하여 int
4 바이트 경계에 정렬되어야 한다고 가정합니다. 즉, 주소는 4의 배수 여야합니다. 주소 buf
가 0x1001
해당 주소를로 변환하는 int *
경우 포인터 값이 제대로 정렬되지 않았기 때문에 유효하지 않습니다. 의 주소 buf
가 0x1000
다음으로 변환 되면 int *
유효합니다.
최신 정보:
추가 한 코드는 정렬 문제를 해결하므로 그 점에서 괜찮습니다. 그러나 다른 문제가 있습니다. 엄격한 앨리어싱을 위반합니다.
정의한 배열에는 유형의 개체가 포함되어 있습니다 char
. 주소를 다른 유형으로 캐스팅 한 다음 변환 된 유형 유형을 역 참조하면 한 유형의 객체를 다른 유형의 객체로 액세스하게됩니다. 이것은 C 표준에서 허용되지 않습니다.
표준에서 "엄격한 앨리어싱"이라는 용어가 사용되지는 않지만이 개념은 섹션 6.5 단락 6 및 7에 설명되어 있습니다.
6 저장된 값에 액세스하기위한 개체 의 유효 유형 은 개체의 선언 된 유형입니다 (있는 경우). 87) 문자 유형이 아닌 유형의 lvalue를 통해 선언 된 유형이없는 객체에 값이 저장되면 lvalue의 유형은 해당 액세스 및 그렇지 않은 후속 액세스에 대한 객체의 유효 유형이됩니다. 저장된 값을 수정하십시오. 값을 사용하여 더 선언 유형을 갖지 않는 개체에 복사되어있는 경우
memcpy
나memmove
, 또는 문자 형태의 배열로 복사되고, 해당 액세스하고 값을 변경하지 않는 후속 액세스에 대한 상기 변형 된 개체의 효과적인 유형은 효과적인 유형은 값이 복사 된 개체의 값입니다 (있는 경우). 선언 된 유형이없는 객체에 대한 다른 모든 액세스의 경우 객체의 유효 유형은 액세스에 사용되는 lvalue의 유형입니다.7 일 : 객체는 저장된 값은 다음의 형식 중 하나를 갖는다 좌변 식에서만 액세스 가진다 88)
- 개체의 유효 유형과 호환되는 유형,
- 객체의 유효 유형과 호환되는 유형의 정규화 된 버전
- 개체의 유효 유형에 해당하는 서명 된 유형 또는 서명되지 않은 유형 인 유형
- 객체의 유효 유형의 규정 된 버전에 해당하는 서명되거나 서명되지 않은 유형 인 유형
- 멤버들 사이에 앞서 언급 한 유형 중 하나를 포함하는 집계 또는 공용체 유형 (재귀 적으로 하위 집계 또는 포함 된 공용체의 멤버 포함), 또는
- 문자 유형.
...
87) 할당 된 객체에는 선언 된 유형이 없습니다.
88)이 목록의 목적은 객체가 별칭을 지정하거나 지정하지 않을 수있는 상황을 지정하는 것입니다.
귀하의 예에서는 객체 위에 unsigned long
및 a double
를 작성하고 char
있습니다. 이러한 유형 중 어느 것도 7 항의 조건을 충족하지 않습니다.
그 외에도 여기에있는 포인터 산술은 유효하지 않습니다.
*(((double *) buf) + 2) = 1.618;
그렇지 않은 경우 buf
의 배열로 취급 하고 double
있습니다. 최소한 필요한 산술을 buf
직접 수행 하고 마지막에 결과를 캐스팅해야합니다.
그렇다면 이것이 왜 char
배열에 대한 문제 이고에 의해 반환되는 버퍼가 malloc
아닌가? 에서 반환 된 메모리는 무언가를 저장할 때까지 유효 유형 malloc
이 없기 때문에 6 항과 각주 87이 설명합니다.
따라서 표준의 엄격한 관점에서 여러분이하는 일은 정의되지 않은 행동입니다. 그러나 컴파일러에 따라 엄격한 앨리어싱을 비활성화 할 수 있으므로 이것이 작동합니다. gcc를 사용하는 경우 -fno-strict-aliasing
플래그 를 전달하고 싶을 것입니다.
표준은 구현시 코드가 T*
유형 T에 대해 정렬되지 않은 값을 관찰 할 가능성을 고려하도록 요구하지 않습니다. 예를 들어, "더 큰"로드 / 저장 명령이 정렬되지 않은 액세스를 지원하지 않는 플랫폼을 대상으로 할 때 clang에서 포인터를 정렬이 충족되지 않는 유형으로 변환 한 다음 사용 memcpy
하면 컴파일러가 코드를 생성 할 수 있습니다.이 코드는 포인터가 정렬되지 않은 경우 실패 memcpy
할 수 있습니다. 그렇지 않으면 정렬 요구 사항을 부과하지 않습니다.
예를 들어 ARM Cortex-M0 또는 Cortex-M3을 대상으로하는 경우 다음과 같습니다.
void test1(long long *dest, long long *src)
{
memcpy(dest, src, sizeof (long long));
}
void test2(char *dest, char *src)
{
memcpy(dest, src, sizeof (long long));
}
void test3(long long *dest, long long *src)
{
*dest = *src;
}
clang은 정렬 src
되거나 dest
정렬되지 않은 경우 실패하는 test1 및 test3 코드 모두에 대해 생성 되지만 test2
더 크고 느린 코드를 생성하지만 소스 및 대상 피연산자의 임의 정렬을 지원합니다.
확실히, clang에서도 정렬되지 않은 포인터를로 변환하는 행위는 long long*
일반적으로 그 자체로 이상한 일을 일으키지 않지만, 그러한 변환은 컴파일러가 처리 할 책임을 면제하는 UB를 생성한다는 사실입니다. .NET의 정렬되지 않은 포인터 케이스 test1
.