programing

Null 포인터 주소에 액세스하는 C 표준 준수 방법?

css3 2023. 10. 22. 20:19

Null 포인터 주소에 액세스하는 C 표준 준수 방법?

C에서 null 포인터를 식별하는 것은 Undefined Behavior이지만, null 포인터 값은 일부 아키텍처에서 유효한 주소(예: 주소 0)를 가리키도록 만드는 비트 표현을 가지고 있습니다.
명확하게 하기 위해 이 주소를 널 포인터 주소라고 합니다.

메모리에 대한 접근이 자유로운 환경에서 C로 소프트웨어를 작성하고 싶다고 가정합니다.Null 포인터 주소에 데이터를 쓰고 싶다고 가정해 보겠습니다. 표준 준수 방식으로 이를 달성하려면 어떻게 해야 할까요?

예제 사례(IA32e):

#include <stdint.h>

int main()
{
   uintptr_t zero = 0;

   char* p = (char*)zero;

   return *p;
}

IA32e용 -O3와 함께 gcc로 컴파일되면 이 코드는 다음과 같이 변환됩니다.

movzx eax, BYTE PTR [0]
ud2

UB로 인해(0은 널 포인터의 비트 표현입니다).

C는 로우 레벨 프로그래밍에 가깝기 때문에 Null pointer address에 접근하여 UB를 피할 수 있는 방법이 있어야 한다고 생각합니다.


확실히 하기 위해
구현 정의 방식으로 이를 달성하는 방법이 아니라 표준이 이에 대해 말하는 것이 무엇인지 묻는 것입니다.
나는 후자의 답을 알고 있습니다.

나는 마음을 비우기 위해 C99 표준을 읽었습니다.제 질문에 관심있는 부분을 찾아서 참고로 씁니다.

사항
저는 완전 초보자입니다. 제가 쓴 글의 90% 이상이 잘못되었거나, 말이 안 되거나, 토스터를 부술지도 모릅니다.저는 또한 표준적인 근거를 만들려고 노력하는데, 종종 (댓글에 나와 있는 것처럼) 처참하고 순진한 결과를 가지고 있습니다.
읽지 마요.
공식적이고 전문적인 답변은 @Olaf에 문의하십시오.

다음의 경우 아키텍처 주소라는 용어는 프로세서에서 볼 수 있는 메모리 주소(논리적, 가상, 선형, 물리적 또는 버스 주소)를 설계했습니다.즉, 어셈블리에서 사용할 주소입니다.


6.3.2.3항에서 다음과 같이 기술합니다.

값이 0인 정수 상수 식 또는 형식에 캐스트되는 식void *, null 포인터 상수라고 합니다.Null 포인터 상수를 포인터 유형으로 변환하면 결과적으로 Null 포인터라고 불리는 포인터는 어떤 개체나 함수에 대한 포인터와 비교할 수 없습니다.

정수에서 포인터로 변환하는 것과 관련하여.

정수는 임의의 포인터 유형으로 변환할 수 있습니다.이전에 지정한 [null pointer constant]경우를 제외하고, 결과가 구현에 정의되어 있고, 올바르게 정렬되어 있지 않을 수 있으며, 참조된 유형의 엔티티를 가리키지 않을 수 있으며, 트랩 표현일 수 있습니다.

이것은 컴파일러가 준수하기 위해 정수에서 포인터로 함수 int2ptr을 구현하기만 하면 된다는 것을 의미합니다.

  1. int2ptr(0)은 정의상 널 포인터입니다.
    int2ptr(0)이 0이어야 하는 것은 아닙니다.어떤 비트의 표현이든 가능합니다.
  2. *int2ptr(n!= 0)에는 제약 조건이 없습니다.
    이것은 int2ptr이 identity 함수일 필요가 없고 유효한 포인터를 반환하는 함수일 필요가 없다는 것을 의미합니다!

아래의 코드를 감안할 때

char* p = (char*)241;

표준은 표현식을 절대적으로 보장하지 않습니다.*p = 56;건축 주소 241에 쓸 것입니다.
따라서 다른 아키텍처 주소(유효한 경우 널 포인터로 설계된 주소인 int2ptr(0) 포함)에 직접 액세스할 수 있는 방법을 제공하지 않습니다.

간단히 말하면, 표준은 아키텍처 주소를 다루는 것이 아니라 포인터, 비교, 변환 및 연산을 다룹니다.

우리가 코드를 쓸 때는.char* p = (char*)K우리는 컴파일러에게 다음을 만들라고 말하지 않습니다.p 아키텍처 주소 K를 가리키면, 우리는 그것을 정수 K로 포인터를 만들거나 다른 용어로 만들라고 말하고 있습니다.p(C 초록) 주소 K를 가리킵니다.

Null 포인터와 (아키텍처) 주소 0x0은 동일하지 않으므로 정수 K와 (아키텍처) 주소 K에서 만들어진 다른 포인터에 대해서도 마찬가지입니다.

어떤 이유에서인지, 어린 시절의 유산, 저는 C의 정수 리터럴을 사용하여 건축 주소를 표현할 수 있다고 생각했습니다. 그 대신 제가 틀렸고 제가 사용하고 있던 컴파일러에서만 그것이 옳았습니다.

제 자신의 질문에 대한 답은 간단히 다음과 같습니다.C 표준 문서에는 (건축) 주소가 없기 때문에 표준 방법이 없습니다.이는 int2ptr(0) 주소뿐만1 아니라 모든 (아키텍처) 주소에 적용됩니다.


참고 사항return *(volatile char*)0;

표준은 다음과 같이 말합니다.

포인터에 잘못된 값 [null 포인터 값이 잘못된]이 할당된 경우 unary * 연산자의 동작이 정의되지 않습니다.

그 밖에

따라서 이러한 [휘발성] 객체를 지칭하는 표현은 추상 기계의 규칙에 따라 엄격하게 평가되어야 합니다.

추상적인 기계는 다음과 같이 말합니다.*null 포인터 값에 대해 정의되지 않으므로 코드가 이 값과 다르지 않아야 합니다.

return *(char*)0;

또한 정의되지 않았습니다.
적어도 GCC 4.9에서는 둘 다 질문에 명시된 지침에 따라 컴파일됩니다.

0 아키텍처 주소에 액세스하는 구현 정의된 방법은 GCC의 경우 "예상" 어셈블리 코드를 생성하는 -fno-isolate-errorous-paths-dereference 플래그를 사용하는 것입니다.


포인터를 정수로 또는 정수를 포인터로 변환하기 위한 매핑 함수는 실행 환경의 어드레싱 구조와 일치하도록 의도됩니다.

불행하게도 그 말은&피연산자의 주소를 산출합니다. 이것은 좀 부적절하다고 생각합니다. 피연산자에 대한 포인터를 산출한다고 할 수 있습니다.변수 고려a주소 0 xf1에 16비트 주소 공간에 상주하는 것으로 알려져 있으며 int2ptr(n) = 0x8000 | n을 구현하는 컴파일러를 고려합니다.&a비트 표현이 0x80f1인 포인터를 생성할 것이며 이는 주소가 아닙니다.a.

제가 구현한 것 중에서 유일하게 접근할 수 없었기 때문에 1특별한 것이었습니다.

OP가 자신의 질문에 대한 답변에서 올바르게 결론을 내린 바와 같이:

C 표준 문서에는 (건축) 주소가 없기 때문에 표준 방법이 없습니다.이는 int2ptr(0) 주소뿐만 아니라 모든 (아키텍처) 주소에 적용됩니다.

그러나 메모리에 직접 액세스하고자 하는 상황은 맞춤형 링커 스크립트를 사용하는 경우일 가능성이 높습니다.(즉, 일종의 임베디드 시스템 같은 것).따라서 OP가 요청하는 표준 준수 방식은 링커 스크립트에서 (아키텍처) 주소에 대한 기호를 내보내고 C 코드 자체의 정확한 주소를 신경 쓰지 않는 것입니다.

이 방식의 변형은 주소 0에 기호를 정의하고 단순히 이 기호를 사용하여 다른 필요한 주소를 도출하는 것입니다.이 작업을 수행하려면 다음과 같은 내용을 추가합니다.SECTIONS링커 스크립트의 일부(GNU ld 구문 가정):

_memory = 0;

그리고 당신의 C 코드에:

extern char _memory[];

이제 예를 들어 예를 들어 제로 주소에 대한 포인터를 생성할 수 있습니다.char *p = &_memory[0];(또는 간단히)char *p = _memory;), int를 포인터로 변환하지 않습니다.유사하게,int addr = ...; char *p_addr = &_memory[addr];주소에 대한 포인터를 생성할 것입니다.addr기술적으로 포인터에 힌트를 주지 않고 말입니다.

링커는 C 표준 및 C 컴파일러와 독립적이고 모든 링커는 자신의 링커 스크립트에 대해 다른 구문을 가질 수 있기 때문에 이것은 당연히 원래 질문을 피합니다.또한 컴파일러가 액세스되는 주소를 인식하지 못하기 때문에 생성된 코드의 효율성이 떨어질 수 있습니다.하지만 이것은 여전히 질문에 흥미로운 관점을 더한다고 생각하기 때문에 약간 주제를 벗어난 답변을 용서해 주시기 바랍니다.)

어떤 솔루션이든 구현에 의존적입니다.꼭 필요합니다.ISO C는 C 프로그램이 실행되는 환경을 설명하는 것이 아니라 다양한 환경(« 데이터 처리 시스템 ») 중에서 적합한 C 프로그램의 모습을 설명합니다.이 표준은 개체 배열이 아닌 주소에 액세스하여 얻을 수 있는 것, 즉 환경이 아닌 눈에 보이는 할당된 것을 보장할 수 없습니다.

따라서 표준에서 정의되지 않은 동작이 아닌 구현 정의된 것(조건부 지원되는 것까지)을 사용할 것입니다.*: 인라인 어셈블리.GCC/clang의 경우:

asm volatile("movzx 0, %%eax;") // *(int*)0;

또한 당신이 있는 것처럼 보이는 독립적인 환경에 대해서도 언급할 가치가 있습니다.표준은 이 실행 모델(empassis mine)에 대해 다음과 같이 말합니다.

§ 5.1.2

실행 환경은 두 가지로 정의됩니다. 즉, 자유로운 스탠딩(free standing)과 호스트(hosting). [...]

§ 5.1.2.1, 쉼표 1

(운영 체제의 이점 없이 C 프로그램 실행이 이루어질있는) 자유로운 스탠딩 환경에서는 프로그램 시작 시 호출되는 함수의 이름과 유형이 구현 정의됩니다.제4항에서 요구하는 최소 집합을 제외하고, 독립형 프로그램이 이용할 수 있는 모든 라이브러리 시설은 구현에 정의되어 있습니다. [...]

어떤 주소에도 마음대로 접근할 수 없다고 적혀 있지는 않습니다.


그게 무슨 뜻이든 간에.표준 딜러가 관리하는 구현 방식을 사용할 때는 상황이 조금씩 다릅니다.

모든 인용문은 N. 1570호 초안에 의거한 것입니다.

C 표준은 구현이 어떤 형태로든 정수와 비슷한 주소를 가질 것을 요구하지 않습니다. 만약 타입 uintr_t와 intptr_t가 존재한다면, 포인터를 uintr_t 또는 intptr_t로 변환하는 행위가 숫자를 산출할 것입니다.그리고 그 숫자를 원래 포인터와 같은 유형으로 바로 다시 변환하면 원래와 같은 포인터가 됩니다.

정수와 유사한 주소를 사용하는 플랫폼은 이러한 매핑에 익숙한 사용자가 놀랄 일이 아닌 방식으로 정수와 주소 간의 변환을 정의하는 것이 권장되지만, 이는 요구 사항이 아니며 이러한 권장 사항에 의존하는 코드는 엄격하게 준수되지 않습니다.

그럼에도 불구하고, 저는 품질 구현이 단순한 비트 와이즈 매핑에 의해 정수 대 포인터 변환을 수행한다고 명시한다면, 그리고 코드가 주소 0에 접근하고자 하는 그럴듯한 이유가 있을 수 있다면, 다음과 같은 문장을 고려해야 한다고 제안합니다.

*((uint32_t volatile*)0) = 0x12345678;
*((uint32_t volatile*)x) = 0x12345678;

주소 0과 주소 x에 쓰기 요청으로서, x가 0이 되더라도, 그리고 구현이 일반적으로 널 포인터 액세스에 트랩되는 경우에도 그 순서로 쓰기를 요청합니다.표준이 포인터와 정수 간의 매핑에 대해 아무런 언급을 하지 않는 한 이러한 동작은 "표준"이 아니지만, 그럼에도 불구하고 좋은 품질의 구현은 현명하게 동작해야 합니다.

당신이 묻고 있는 질문은 다음과 같습니다.

해당 메모리에 대한 포인터가 널 포인터와 동일하게 표현되도록 메모리에 액세스하려면 어떻게 해야 합니까?

이 표준의 문자 그대로의 판독에 따르면, 이것은 불가능합니다. 6.3.2.3/3은 물체에 대한 어떤 포인터도 널 포인터와 동등하지 않다고 말합니다.

따라서 이 포인터는 개체를 가리켜서는 안 됩니다.하지만 존경 연산자는*, 개체 포인터에 적용되며 개체를 가리키는 경우에만 동작을 지정합니다.


그렇다고 해서 C의 객체 모델이 엄격하게 명시된 적은 없기 때문에 위와 같은 해석에 큰 비중을 두지는 않을 것입니다.그럼에도 불구하고, 제가 보기에는 당신이 생각하는 어떤 해결책이든 사용 중인 컴파일러의 비표준 동작에 의존해야 할 것 같습니다.

우리는 gcc의 옵티마이저가 처리의 늦은 단계에서 모든 비트-제로 포인터를 감지하고 UB로 플래그를 지정하는 다른 답변에서 이것의 예를 볼 수 있습니다.

언급URL : https://stackoverflow.com/questions/35537579/c-standard-compliant-way-to-access-null-pointer-address