IT이야기

&((구조명*)NULL -> b)는 C11에서 정의되지 않은 동작을 야기합니까?

cyworld 2022. 6. 18. 09:30
반응형

&((구조명*)NULL -> b)는 C11에서 정의되지 않은 동작을 야기합니까?

코드 샘플:

struct name
{
    int a, b;
};

int main()
{
    &(((struct name *)NULL)->b);
}

이로 인해 정의되지 않은 동작이 발생합니까?우리는 그것이 "참조 무효"인지 아닌지에 대해 논의할 수 있지만, C11은 "참조 무효"라는 용어를 정의하지 않는다.

6.5.3.2/4에 따르면*null 포인터에서는 정의되지 않은 동작이 발생합니다.단, 같은 것을 나타내는 것은 아닙니다.->또, 이 정의도 정의하지 않습니다.a -> b있는 그대로(*a).b; 각 연산자에 대해 별도의 정의가 있습니다.

의 의미->6.5.2.3/4에서는 다음과 같이 기술되어 있습니다.

-> 연산자 뒤에 이어지는 postfix 식과 식별자는 구조체 또는 결합 객체의 멤버를 나타냅니다.값은 첫 번째 식이 가리키는 객체의 명명된 멤버의 값이며 l값입니다.

하지만,NULL목적어를 가리키지 않기 때문에 두 번째 문장은 명기되어 있지 않은 것 같습니다.

6.5.3.2/1도 관련될 수 있습니다.

제약사항:

단항 피연산자&연산자는 기능 지정자 중 하나여야 한다.[]또는 단항*연산자 또는 레지스터 스토리지 클래스 지정자로 선언되지 않은 비트 필드가 아닌 개체를 지정하는 l값입니다.

단, 굵은 글씨로 표시된 텍스트는 결함이 있어 6.3.2.1/1(lvalue의 정의)에 따라 오브젝트를 지정할 가능성이 있는lvalue를 읽어야 한다고 생각합니다.C99는 lvalue의 정의를 엉망으로 하고 있기 때문에 C11은 이 섹션을 다시 작성해야 했습니다.

6.3.2.1/1은 다음과 같이 말한다.

lvalue는 객체를 잠재적으로 지정하는 표현식(void 이외의 객체 유형 포함)입니다.lvalue가 평가될 때 객체를 지정하지 않으면 동작이 정의되지 않습니다.

하지만, 그&연산자는 피연산자를 평가합니다.(저장된 값에 액세스하지 않지만 다른 값입니다).

이 긴 추론 사슬은 코드가 UB를 야기하는 것처럼 보이지만 그것은 상당히 미약하고 표준 작성자들이 무엇을 의도했는지 내게는 명확하지 않다.만약 그들이 실제로 우리에게 토론에 맡기는 것이 아니라, 무언가를 의도했다면:)

우선 간접 연산자부터 시작합시다.*:

6.5.3.2 p4: unary * 연산자는 indirection을 나타냅니다.피연산자가 함수를 가리킬 경우 결과는 함수 지정자가 되고 객체를 가리킬 경우 결과는 객체를 나타내는 l값이 됩니다.피연산자의 타입이 "point to type"인 경우 결과 타입은 "type"입니다. 포인터에 비활성값이 할당되어 있는 경우, 단항 연산자의 동작은 정의되지 않습니다.102).

*E(E는 늘 포인터)는 정의되지 않은 동작입니다.

다음과 같은 각주가 있습니다.

102) 즉,E(E가 늘 포인터라도), &(E1[E2])~(E1)+(E2)에 상당한다.E가 함수 지정자 또는 단항 연산자의 유효한 피연산자인 l값인 경우 *&E는 함수 지정자 또는 E와 동일한 l값인 것은 항상 사실입니다.*P가 lvalue이고 T가 오브젝트 포인터 타입의 이름일 경우 *(T)P는 T가 가리키는 타입과 호환되는 타입을 가진 lvalue입니다.

즉, &*E(E는 NULL)이 정의되어 있는데, 문제는 &(*E.m)에 대해서도 동일한지 여부입니다.여기서 E는 늘 포인터이고 그 유형은 멤버 m을 가진 구조체입니다.

C 표준에서는 그 동작을 정의하지 않습니다.

정의되어 있는 경우, 새로운 문제가 발생합니다.그 중 하나는 다음과 같습니다.C Standard는 정의되지 않은 상태로 유지되며 문제를 내부적으로 처리하는 매크로 오프셋을 제공합니다.

6.3.2.3 포인터

  1. 값이 0인 정수 상수 표현식 또는 void* 타입으로 주조된 표현식은 늘 포인터 상수라고 불립니다.66) 늘 포인터 상수가 포인터 타입으로 변환되면 늘 포인터라고 불리는 결과 포인터는 객체 또는 함수에 대한 포인터와 동등하지 않음을 보증합니다.

즉, 값이 0인 정수 상수식이 늘 포인터 상수로 변환됩니다.

그러나 늘 포인터 상수 값은 0으로 정의되지 않습니다.값은 구현 정의입니다.

7.19 일반적인 정의

  1. 매크로는 NULL이며 구현 정의 NULL 포인터 상수로 확장됩니다.

즉, C는 null 포인터가 모든 비트가 설정된 값을 가지며, 이 값에 대한 멤버액세스를 사용하면 정의되지 않은 동작인 오버플로가 발생합니다.

또 다른 문제는 &(*E.m)을 어떻게 평가하느냐는 것입니다.브래킷이 적용되어 있습니까?*먼저 평가합니다.정의되지 않은 상태로 두면 이 문제가 해결됩니다.

변호사의 관점에서 보면, 그 표현은&(((struct name *)NULL)->b);UB가 없는 경로를 찾을 수 없었기 때문에 UB로 연결됩니다.IMHO의 근본 원인은 당신이 순간적으로 적용하기 때문입니다.->개체를 가리키지 않는 식의 연산자.

컴파일러의 관점에서 보면 컴파일러 프로그래머가 지나치게 복잡하지 않다고 가정할 때 표현식이 다음과 같은 값을 반환하는 것은 분명하다.offsetof(name, b)오류 없이 컴파일된다면 기존 컴파일러가 그 결과를 얻을 수 있을 것입니다.

기술한 바와 같이, 내부 부분에서 연산자를 사용하는 것을 지적하는 컴파일러를 탓할 수는 없습니다.->(null이기 때문에) 개체를 가리키고 경고 또는 오류를 발생시킬 수 없는 식입니다.

내 결론은 주소를 받는 것만이 합법이라면 늘 포인터를 참조해 달라는 특별한 단락이 있을 때까지 이 표현은 합법 C가 아니라는 것이다.

먼저 오브젝트에 대한 포인터가 필요하다는 것을 확인합니다.

6.5.2.3 구조 및 조합원

4 포스트픽스 표현 뒤에 이어지는->연산자 및 식별자는 구조물 또는 결합 객체의 구성원을 나타냅니다.값은 첫 번째 식이 가리키는 객체의 명명된 멤버의 값이며 l값입니다.96) 첫 번째 표현이 한정 타입에 대한 포인터일 경우 결과는 지정된 멤버 타입의 한정 버전을 가진다.

유감스럽게도 null 포인터는 개체를 가리킬 수 없습니다.

6.3.2.3 포인터

3 값이 0인 정수 상수 표현식 또는 형식으로 주조된 표현식void *를 늘 포인터 상수라고 부릅니다.66) 늘 포인터 상수가 포인터 유형으로 변환되면 늘 포인터라고 불리는 결과 포인터는 오브젝트 또는 함수에 대한 포인터와 동등하지 않음을 보증합니다.

결과: 정의되지 않은 동작.

덧붙여서, 그 외의 몇 가지 주의할 점은 다음과 같습니다.

6.3.2.3 포인터

4 Null 포인터를 다른 포인터 타입으로 변환하면 해당 타입의 Null 포인터가 생성됩니다.임의의 두 개의 늘 포인터는 동등하게 비교해야 한다.
5 정수는 임의의 포인터 타입으로 변환할 수 있다.이전에 지정한 경우를 제외하고 결과는 구현 정의이며 올바르게 정렬되지 않을 수 있으며 참조된 유형의 엔티티를 가리키지 않을 수 있으며 트랩 표현일 수 있습니다.67)
6 어떤 포인터 타입도 정수 타입으로 변환할 수 있다.이전에 지정한 경우를 제외하고 결과는 구현 정의됩니다.결과를 정수 유형으로 나타낼 수 없는 경우 동작은 정의되지 않습니다.결과는 정수 유형의 값 범위 내에 있을 필요가 없습니다.

67) 포인터를 정수로 변환하거나 포인터로 변환하는 매핑 기능은 실행 환경의 어드레싱 구조와 일치하도록 한다.

따라서 이번에 UB가 양성이라고 해도 전혀 예상치 못한 수치가 나올 수 있습니다.

네, 이 용도는->정의되지 않은 영어 용어의 직접적인 의미에서 정의되지 않은 동작을 가지고 있습니다.

동작은 첫 번째 식이 개체를 가리킬 때만 정의되고, 그렇지 않으면 정의되지 않습니다(=displays).일반적으로 정의되지 않은 용어로 더 많이 검색해서는 안 됩니다.그것은 단지, 표준이 코드에 의미를 부여하지 않는 것을 의미합니다.(경우에 따라서는 정의되지 않은 상황을 명시적으로 가리킬 수도 있지만, 이것이 용어의 일반적인 의미를 바꾸지는 않습니다.)

이는 컴파일러 빌더가 작업을 처리하는 데 도움이 되기 위해 도입된 느슨함입니다.사용자가 제시하는 코드에 대해서도 동작을 정의할 수 있습니다.특히 컴파일러 구현의 경우 이러한 코드 또는 유사한 코드를 사용하는 것이 좋습니다.offsetof매크로. 이 코드를 제약 조건 위반으로 만들면 컴파일러 구현에 대한 경로가 차단됩니다.

아니, 이걸 분해해 보자.

&(((struct name *)NULL)->b);

다음과 같습니다.

struct name * ptr = NULL;
&(ptr->b);

첫 번째 줄은 분명 유효하고 잘 정의되어 있습니다.

두 번째 줄에서는 주소에 대한 필드의 주소를 계산합니다.0x0그것도 완벽하게 합법적이죠.예를 들어 Amiga는 주소의 커널에 대한 포인터를 가지고 있습니다.0x4이러한 방법으로 커널 함수를 호출할 수 있습니다.

실제로 C 매크로에서도 같은 접근방식이 사용됩니다.offsetof(표준):

#define offsetof(st, m) ((size_t)(&((st *)0)->m))

따라서 여기서의 혼란은 NULL 포인터가 무섭다는 사실을 중심으로 전개됩니다.그러나 컴파일러와 표준적인 관점에서 이 표현은 C에서 합법적이다(C++는 오버로드 할 수 있기 때문에 다른 짐승이다).&오퍼레이터).

C 표준의 어떤 것도 시스템이 이 표현으로 무엇을 할 수 있는지에 대한 요구사항을 부과하지 않습니다.표준이 작성되었을 때 실행 시 다음과 같은 일련의 이벤트를 발생시키는 것은 지극히 합리적입니다.

  1. 코드가 주소 지정 장치에 늘 포인터를 로드합니다.
  2. 코드가 주소 지정 장치에 필드의 오프셋을 추가하도록 요청합니다.b.
  3. 어드레싱 유닛은 늘 포인터에 정수를 추가하려고 할 때 트랩을 트리거합니다(많은 시스템이 이를 포착하지 않더라도 견고성을 위해서는 런타임 트랩이어야 합니다).
  4. 트랩을 설정하는 코드는 주소 지정 트랩이 발생하지 않기 때문에 메모리 낭비가 되기 때문에 시스템은 설정되지 않은 트랩 벡터를 통해 디스패치된 후 기본적으로 랜덤한 코드를 실행하기 시작합니다.

그 당시 정의되지 않은 행동이 의미하는 바로 그 본질입니다.

C 초기부터 등장한 대부분의 컴파일러는 일정한 주소에 있는 오브젝트 멤버의 주소를 컴파일 시간 상수로 간주하지만, 그 때는 그러한 동작이 필수적이지 않았다고 생각되며, 컴파일 시간 주소 계산 인볼루트를 의무화하는 표준에도 추가되지 않았습니다.ving null 포인터는 런타임 계산이 정의되지 않는 경우에 정의됩니다.

언급URL : https://stackoverflow.com/questions/26906621/does-struct-name-null-b-cause-undefined-behaviour-in-c11

반응형