NULL 재정의


118

주소 0x0000이 유효하고 포트 I / O가 포함 된 시스템의 C 코드를 작성하고 있습니다. 따라서 NULL 포인터에 액세스하는 모든 가능한 버그는 감지되지 않은 상태로 유지되며 동시에 위험한 동작을 유발합니다.

이러한 이유로 NULL을 다른 주소 (예 : 유효하지 않은 주소)로 재정의하고 싶습니다. 실수로 이러한 주소에 액세스하면 오류를 처리 할 수있는 하드웨어 인터럽트가 발생합니다. 이 컴파일러의 stddef.h에 액세스 할 수 있으므로 실제로 표준 헤더를 변경하고 NULL을 다시 정의 할 수 있습니다.

제 질문은 이것이 C 표준과 충돌할까요? 표준의 7.17에서 알 수있는 한 매크로는 구현에 따라 정의됩니다. 표준에 NULL 이 0 이어야 한다는 내용이 있습니까?

또 다른 문제는 많은 컴파일러가 데이터 유형에 관계없이 모든 것을 0으로 설정하여 정적 초기화를 수행한다는 것입니다. 표준에 따르면 컴파일러는 정수를 0으로 설정하고 포인터를 NULL로 설정해야합니다. 내 컴파일러에 대해 NULL을 다시 정의하면 이러한 정적 초기화가 실패한다는 것을 알고 있습니다. 컴파일러 헤더를 수동으로 대담하게 변경 했음에도 불구하고 잘못된 컴파일러 동작으로 간주 할 수 있습니까? 이 특정 컴파일러가 정적 초기화를 수행 할 때 NULL 매크로에 액세스하지 않는다는 것을 확실히 알고 있기 때문입니다.


3
이것은 정말 좋은 질문입니다. 나는 당신을위한 답을 가지고 있지는 않지만 질문해야합니다. 0x00에서 유효한 물건을 멀리 옮기고 NULL을 "일반"시스템 에서처럼 잘못된 주소로 두는 것이 불가능하다고 확신합니까? 그렇게 할 수 없다면 사용할 수있는 안전한 유효하지 않은 주소 는 할당 할 수 있는지 확인한 다음 mprotect보안 할 수있는 주소뿐입니다 . 또는 플랫폼에 ASLR 등이없는 경우 플랫폼 물리적 메모리를 초과하는 주소입니다. 행운을 빕니다.
Borealid

8
코드가 사용중인 경우 어떻게 작동 if(ptr) { /* do something on ptr*/ }합니까? NULL이 0x0과 다르게 정의되면 작동합니까?
Xavier T.

3
C 포인터는 메모리 주소와 강제 관계가 없습니다. 포인터 산술의 규칙이 준수되는 한 포인터 값은 무엇이든 될 수 있습니다. 대부분의 구현은 메모리 주소를 포인터 값으로 사용하도록 선택하지만 동형이있는 한 무엇이든 사용할 수 있습니다.
datenwolf

2
@bdonlan MISRA-C의 (자문) 규칙도 위반합니다.
Lundin

2
@Andreas 그래 그게 내 생각입니다. 하드웨어 담당자가 소프트웨어가 실행되어야하는 하드웨어를 설계하도록 허용해서는 안됩니다! :)
Lundin

답변:


84

C 표준은 널 포인터가 기계의 주소 0에있을 것을 요구하지 않습니다. 그러나 0상수를 포인터 값으로 캐스팅하면 NULL포인터 (§6.3.2.3 / 3)가 생성되어야하며 null 포인터를 부울로 평가하는 것은 false 여야합니다. 이것은 당신이 정말로 경우 조금 어색 할 수 있습니다 않는 제로 주소를 원하고, NULL제로 주소가 아닙니다.

그럼에도 불구하고 컴파일러 및 표준 라이브러리에 대한 (무거운) 수정으로 표준 라이브러리를 NULL엄격하게 준수하면서 대체 비트 패턴으로 표현하는 것이 불가능하지 않습니다 . 그러나 자신 의 정의를 단순히 변경하는 것만으로 는 충분 하지 않습니다 .NULLNULL

특히 다음을 수행해야합니다.

  • 포인터에 대한 할당 (또는 포인터에 대한 캐스트)의 리터럴 0이 -1.
  • 0대신 매직 값을 확인하기 위해 포인터와 상수 정수 간의 동등성 테스트를 정렬 합니다 (§6.5.9 / 6).
  • 포인터 유형이 부울로 평가되는 모든 컨텍스트를 정렬하여 0을 확인하는 대신 매직 값과 같은지 확인합니다. 이것은 동등성 테스트 의미론을 따르지만 컴파일러는 내부적으로 다르게 구현할 수 있습니다. §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4 참조
  • caf가 지적했듯이 정적 개체 (§6.7.8 / 10) 및 부분 복합 이니셜 라이저 (§6.7.8 / 21) 초기화에 대한 의미 체계를 업데이트하여 새로운 null 포인터 표현을 반영합니다.
  • 실제 주소 0에 액세스하는 다른 방법을 만듭니다.

처리 할 필요가 없는 것들이 있습니다 . 예를 들면 :

int x = 0;
void *p = (void*)x;

그 후에 p는 null 포인터가 보장되지 않습니다. 상수 할당 만 처리하면됩니다 (이는 실제 주소 0에 액세스하기위한 좋은 접근 방식입니다). 마찬가지로:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

또한:

void *p = NULL;
int x = (int)p;

x은 보장되지 않습니다 0.

요컨대,이 조건은 C 언어위원회에 의해 분명히 고려되었으며 NULL에 대한 대체 표현을 선택하는 사람들을 위해 고려되었습니다. 지금해야 할 일은 컴파일러를 크게 변경하는 것뿐입니다.

참고로 컴파일러가 적절하기 전에 소스 코드 변환 단계를 통해 이러한 변경 사항을 구현할 수 있습니다. 즉, 전 처리기-> 컴파일러-> 어셈블러-> 링커의 일반적인 흐름 대신 전 처리기-> NULL 변환-> 컴파일러-> 어셈블러-> 링커를 추가합니다. 그런 다음 다음과 같은 변환을 수행 할 수 있습니다.

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

이를 위해서는 포인터에 해당하는 식별자를 결정하기 위해 유형 파서 및 typedef 및 변수 선언 분석뿐만 아니라 전체 C 파서가 필요합니다. 그러나 이렇게하면 컴파일러의 코드 생성 부분을 적절하게 변경하지 않아도됩니다. clang 은이를 구현하는 데 유용 할 수 있습니다. 이러한 변형을 염두에두고 설계 되었음을 이해합니다. 물론 표준 라이브러리도 변경해야 할 것입니다.


2
좋아, §6.3.2.3의 텍스트를 찾지 못했지만 어딘가에 그러한 진술이있을 것이라고 생각했습니다. :). 나는 이것이 나를 백업하기 위해 새로운 C 컴파일러를 작성하는 것을 좋아하지 않는 한 표준에 의해 NULL을 다시 정의하는 것이 허용되지 않는다는 내 질문에 대답한다고 생각합니다. :)
Lundin

2
좋은 트릭은 컴파일러를 해킹하여 pointer <-> integer 변환이 유효하지 않은 포인터 인 특정 값을 XOR하도록하는 것이며, 대상 아키텍처가이를 저렴하게 수행 할 수있을만큼 충분히 사소한 것입니다 (보통 단일 비트 세트가있는 값입니다). , 예 : 0x20000000).
Simon Richter

2
컴파일러에서 변경해야 할 또 다른 사항은 복합 유형으로 객체를 초기화하는 것입니다. 객체가 부분적으로 초기화 된 경우 명시 적 초기자가없는 포인터는로 초기화해야합니다 NULL.
caf

20

표준은 값이 0 인 정수 상수 표현식 또는 void *유형으로 변환 된 표현식 이 널 포인터 상수 라고 명시합니다 . 이 수단 (void *)0항상 널 포인터이지만, 주어는 int i = 0;, (void *)i필요 없습니다.

C 구현은 헤더와 함께 컴파일러로 구성됩니다. 재정의하기 위해 헤더를 NULL수정하고 정적 초기화를 수정하기 위해 컴파일러를 수정하지 않으면 부적합한 구현을 만든 것입니다. 잘못된 동작을 가진 전체 구현이며, 만약 당신이 그것을 깨뜨렸다면, 당신은 정말로 다른 사람을 비난 할 사람이 없습니다;)

포인터를 부여 - 당신은 물론, 정적 인 initialisations보다 더 수정해야합니다 p, if (p)에 해당 if (p != NULL)인해 위의 규칙.


8

C std 라이브러리를 사용하면 NULL을 반환 할 수있는 함수에 문제가 발생합니다. 예를 들어 malloc 문서 는 다음과 같이 설명 합니다.

함수가 요청 된 메모리 블록을 할당하지 못한 경우 널 포인터가 리턴됩니다.

malloc 및 관련 함수는 이미 특정 NULL 값을 사용하여 바이너리로 컴파일되었으므로 NULL을 재정의하면 C std 라이브러리를 포함하여 전체 도구 체인을 다시 빌드 할 수없는 경우 C std 라이브러리를 직접 사용할 수 없습니다.

또한 std 라이브러리의 NULL 사용으로 인해 std 헤더를 포함하기 전에 NULL을 다시 정의하면 헤더에 나열된 NULL 정의를 덮어 쓸 수 있습니다. 인라인 된 것은 컴파일 된 객체와 일치하지 않습니다.

대신 사용자 고유의 사용을 위해 사용자 고유의 NULL, "MYPRODUCT_NULL"을 정의하고 C std 라이브러리를 피하거나 변환합니다.


6

NULL을 그대로두고 포트 0x0000에 대한 IO를 특수한 경우로 처리합니다. 어셈블러로 작성된 루틴을 사용하므로 표준 C 의미 체계가 적용되지 않습니다. IOW, NULL을 다시 정의하지 말고 포트 0x00000을 다시 정의하십시오.

C 컴파일러를 작성하거나 수정하는 경우 NULL을 역 참조하는 것을 방지하는 데 필요한 작업 (귀하의 경우 CPU가 도움이되지 않는다고 가정)은 NULL이 어떻게 정의 되든 상관없이 동일하므로 NULL을 정의 된 상태로 두는 것이 더 쉽습니다. 0으로, 0이 C에서 역 참조 될 수 없는지 확인하십시오.


포트에 의도적으로 액세스 한 경우가 아니라 실수로 NULL에 액세스 한 경우에만 문제가 발생합니다. 그때 포트 I / O를 재정의하는 이유는 무엇입니까? 이미 정상적으로 작동하고 있습니다.
Lundin

2
실수 여부 @Lundin, NULL을 수행 할 수 있습니다 만을 사용하여 C 프로그램에서 역 참조 *p, p[]또는 p()컴파일러는 IO 포트 0000을 보호하기 위해 그 걱정 할 필요가 있도록.
Apalala

@Lundin 질문의 두 번째 부분 : C 내에서 주소 0에 대한 액세스를 제한하면 포트 0x0000에 도달하는 다른 방법이 필요합니다. 어셈블러로 작성된 함수가이를 수행 할 수 있습니다. C 내에서 포트는 0xFFFF 등으로 매핑 될 수 있지만 함수를 사용하고 포트 번호는 잊어 버리는 것이 가장 좋습니다.
Apalala

3

다른 사람들이 언급 한 것처럼 NULL을 재정의하는 데있어 극도로 어려운 점을 고려하면 잘 알려진 하드웨어 주소에 대한 역 참조재정의 하는 것이 더 쉬울 수 있습니다. 주소를 만들 때 잘 알려진 모든 주소에 1을 추가하여 잘 알려진 IO 포트가 다음과 같도록합니다.

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

관심있는 주소가 함께 그룹화되어 있고 주소에 1을 추가해도 (대부분의 경우에는 안되는) 어떤 것과도 충돌하지 않는다고 안심할 수 있다면 안전하게 수행 할 수 있습니다. 그런 다음 도구 체인 / std lib 및 식을 다음 형식으로 다시 빌드하는 것에 대해 걱정할 필요가 없습니다.

  if (pointer)
  {
     ...
  }

여전히 작동

미친 건 알아,하지만 그냥 아이디어를 던져 버릴 거라고 생각 했어 :).


포트에 의도적으로 액세스 한 경우가 아니라 실수로 NULL에 액세스 한 경우에만 문제가 발생합니다. 그때 포트 I / O를 재정의하는 이유는 무엇입니까? 이미 정상적으로 작동하고 있습니다.
Lundin

@LundIn 나는 당신이 더 고통스럽고 전체 도구 체인을 재 구축하거나 코드의 일부를 변경하는 것을 선택해야한다고 생각합니다.
Doug T.

2

널 포인터의 비트 패턴은 정수 0의 비트 패턴과 같지 않을 수 있습니다. 그러나 널 매크로의 확장은 널 포인터 상수 여야합니다. 즉, 캐스트 될 수있는 값 0의 상수 정수 여야합니다. *).

준수하면서 원하는 결과를 얻으려면 도구 체인을 수정 (또는 구성)해야하지만 달성 가능합니다.


1

당신은 문제를 요구하고 있습니다. 재정의NULLnull이 아닌 값으로 하면 다음 코드가 손상됩니다.

   if (myPointer)
   {
      // myPointer가 null이 아닙니다.
      ...
   }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.