유효성을위한 포인터 테스트 (C / C ++)


91

주어진 포인터가 "유효"한지 (물론 프로그램 적으로) 결정하는 방법이 있습니까? NULL을 확인하는 것은 쉽지만 0x00001234와 같은 것은 어떻습니까? 이러한 종류의 포인터를 역 참조하려고하면 예외 / 충돌이 발생합니다.

크로스 플랫폼 방법이 선호되지만 플랫폼 별 (Windows 및 Linux 용)도 괜찮습니다.

설명을위한 업데이트 : 문제는 부실 / 해제 / 초기화되지 않은 포인터가 아닙니다. 대신 호출자 (문자열에 대한 포인터, 파일 핸들 등)로부터 포인터를 가져 오는 API를 구현하고 있습니다. 호출자는 (의도적으로 또는 실수로) 잘못된 값을 포인터로 보낼 수 있습니다. 충돌을 어떻게 방지합니까?



리눅스에 대한 가장 긍정적 인 대답은 George Carrette가 제시 한 것이라고 생각합니다. 그것으로 충분하지 않다면, 라이브러리에 함수 심볼 테이블을 구축하거나 자체 함수 테이블이있는 사용 가능한 라이브러리의 다른 레벨을 고려하십시오. 그런 다음 정확한 테이블을 확인하십시오. 물론 이러한 부정적인 대답도 정확합니다. 사용자 응용 프로그램에 많은 추가 제한을 적용하지 않는 한 함수 포인터가 유효한지 여부를 100 % 확신 할 수 없습니다.
minghua 2015

API 사양은 실제로 구현시 충족해야 할 의무를 명시합니까? 그건 그렇고, 나는 당신이 개발자이자 디자이너라고 가정하지 않은 척합니다. 내 요점은 API가 "유효하지 않은 포인터가 인수로 전달되는 경우 함수가 문제를 처리하고 NULL을 반환해야합니다."와 같은 것을 지정하지 않는다는 것입니다. API는 해킹이 아닌 적절한 사용 조건에서 서비스를 제공 할 의무가 있습니다. 그럼에도 불구하고 약간의 어리 석음에 해를 끼치 지 않습니다. 참조를 사용하면 그러한 경우가 덜 혼란 스럽습니다. :)
Poniros

답변:


75

설명을위한 업데이트 : 문제는 오래된 포인터, 해제되었거나 초기화되지 않은 포인터가 아닙니다. 대신 호출자 (문자열에 대한 포인터, 파일 핸들 등)로부터 포인터를 가져 오는 API를 구현하고 있습니다. 호출자는 (의도적으로 또는 실수로) 잘못된 값을 포인터로 보낼 수 있습니다. 충돌을 어떻게 방지합니까?

당신은 그 수표를 만들 수 없습니다. 포인터가 "유효"한지 확인할 수있는 방법은 없습니다. 사람들이 포인터를받는 함수를 사용할 때 그 사람들이 자신이 무엇을하는지 안다는 것을 믿어야합니다. 포인터 값으로 0x4211을 전달하면 0x4211 주소를 가리키는 것을 신뢰해야합니다. 그리고 그들이 "우연히"물체에 부딪힌 경우, 무서운 운영 체제 기능 (IsValidPtr 등)을 사용하더라도 여전히 버그에 빠질 것이며 빠르게 실패하지 않을 것입니다.

이런 종류의 신호를 보내기 위해 널 포인터를 사용하고 라이브러리 사용자에게 실수로 잘못된 포인터를 전달하는 경향이있는 경우 포인터를 사용해서는 안된다고 알리십시오.


이것은 아마도 정답 일 것입니다. 그러나 일반적인 hexspeak 메모리 위치를 확인하는 간단한 함수가 일반적인 디버깅에 유용 할 것이라고 생각합니다. 지금은 가끔 0xfeeefeee를 가리키는 포인터가 있고 제가 할 수있는 간단한 함수가 있다면 주위에 후추 주장을 사용하면 범인을 찾는 것이 훨씬 더 쉬워 질 것입니다 ... 편집 : 자신이 직접 작성하는 것이 어렵지는 않지만 ..
quant

@quant 문제는 ​​일부 C 및 C ++ 코드가 확인하지 않고 유효하지 않은 주소에 대해 포인터 산술을 수행 할 수 있다는 것입니다 (garbage-in, garbage-out 원칙을 기반으로 함). 따라서 그 우물 중 하나에서 "산술적으로 수정 된"포인터를 전달합니다. -알려진 잘못된 주소. 잘못된 객체 주소 또는 잘못된 유형 중 하나를 기반으로 존재하지 않는 vtable에서 메소드를 찾거나 단순히 포인터에서 하나를 가리 키지 않는 구조체에 대한 필드를 읽는 일반적인 경우입니다.
rwong

이것은 기본적으로 외부 세계에서만 배열 인덱스를 가져올 수 있음을 의미합니다. 호출자로부터 자신을 방어해야하는 API는 인터페이스에 포인터를 가질 수 없습니다. 그러나 포인터의 유효성 (내부적으로 가지고 있어야 함)에 대한 단언에 사용할 매크로를 사용하는 것이 좋습니다. 포인터가 시작점과 길이가 알려진 배열 내부를 가리 키도록 보장되는 경우 명시 적으로 확인할 수 있습니다. deref (문서화되지 않은 오류)보다 assert 위반 (문서화 된 오류)으로 죽는 것이 낫습니다.
Rob

34

다음은 Linux에서 C 프로그램이 실행중인 메모리의 상태에 대해 성찰 할 수있는 세 가지 쉬운 방법과 질문이 일부 컨텍스트에서 적절한 정교한 답변을 갖는 이유입니다.

  1. getpagesize ()를 호출하고 포인터를 페이지 경계로 반올림 한 후 mincore ()를 호출하여 페이지가 유효한지 여부와 프로세스 작업 집합의 일부인지 확인할 수 있습니다. 여기에는 약간의 커널 리소스가 필요하므로 벤치마킹하고이 함수를 호출하는 것이 API에서 정말 적절한 지 결정해야합니다. API가 인터럽트를 처리하거나 직렬 포트에서 메모리로 읽는 경우 예측할 수없는 동작을 피하기 위해 이것을 호출하는 것이 적절합니다.
  2. 사용 가능한 / proc / self 디렉토리가 있는지 확인하기 위해 stat ()를 호출 한 후 / proc / self / maps를 열고 포인터가있는 영역에 대한 정보를 찾을 수 있습니다. 프로세스 정보 의사 파일 시스템 인 proc의 매뉴얼 페이지를 살펴보십시오. 분명히 이것은 상대적으로 비용이 많이 들지만 이진 검색을 사용하여 효율적으로 조회 할 수있는 배열로 구문 분석 결과를 캐싱하는 것을 피할 수 있습니다. / proc / self / smaps도 고려하십시오. API가 고성능 컴퓨팅을위한 것이라면 프로그램은 비 균일 메모리 아키텍처 인 numa의 man 페이지에 문서화 된 / proc / self / numa에 대해 알고 싶어 할 것입니다.
  3. get_mempolicy (MPOL_F_ADDR) 호출은 실행 스레드가 여러 개 있고 CPU 코어 및 소켓 리소스와 관련하여 비 균일 메모리에 대한 선호도를 갖도록 작업을 관리하는 고성능 컴퓨팅 API 작업에 적합합니다. 물론 이러한 API는 포인터가 유효한지 여부도 알려줍니다.

Microsoft Windows에는 Process Status API (NUMA API에도 있음)에 문서화 된 QueryWorkingSetEx 함수가 있습니다. 정교한 NUMA API 프로그래밍의 결과로서이 함수를 사용하면 간단한 "유효성 테스트 포인터 (C / C ++)"작업을 수행 할 수 있습니다. 따라서 최소 15 년 동안 사용되지 않을 가능성이 높습니다.


13
질문 자체에 대해 도덕적이지 않고 실제로 완벽하게 대답하는 첫 번째 답변입니다. 사람들은 때때로 타사 라이브러리 또는 레거시 코드에서 버그를 찾기 위해 이러한 종류의 디버깅 접근 방식이 정말로 필요하다는 사실을 깨닫지 못합니다. 왜냐하면 valgrind조차도 실제로 액세스 할 때만 와일드 포인터를 찾습니다. 예를 들어 포인터의 유효성을 정기적으로 확인하려는 경우에는 코드에서 다른 장소에서 덮어왔다 캐시 테이블 ...에
lumpidu

이것은 받아 들여진 대답이어야합니다. 나는 리눅스가 아닌 플랫폼에서 비슷하게했다. 기본적으로 프로세스 정보를 프로세스 자체에 노출합니다. 이러한 측면에서 보면 Windows는 프로세스 상태 API를 통해 더 의미있는 정보를 노출함으로써 Linux보다 더 나은 작업을 수행합니다.
minghua 2015

31

호출자가 잘못된 포인터를 전송하여 발생하는 크래시를 방지하는 것은 찾기 어려운 조용한 버그를 만드는 좋은 방법입니다.

API를 사용하는 프로그래머가 코드를 숨기지 않고 충돌시켜 가짜라는 명확한 메시지를 얻는 것이 더 낫지 않습니까?


8
하지만 어떤 경우에는 API를 호출 즉시 나쁜 포인터를 검사하는 것입니다 일찍 실패하는 방법. 예를 들어, API가 나중에 만 따를 데이터 구조에 포인터를 저장하면 어떻게 될까요? 그런 다음 API에 잘못된 포인터를 전달하면 나중에 임의의 지점에서 충돌이 발생합니다. 이 경우 원래 잘못된 값이 도입 된 API 호출에서 더 빨리 실패하는 것이 좋습니다.
peterflynn 2015 년

28

Win32 / 64에는이를 수행하는 방법이 있습니다. 포인터 읽기를 시도하고 실패시 발생하는 결과 SEH 예외를 포착합니다. 던지지 않으면 유효한 포인터입니다.

이 방법의 문제는 포인터에서 데이터를 읽을 수 있는지 여부를 반환한다는 것입니다. 형식 안전성이나 기타 불변성을 보장하지 않습니다. 일반적으로이 방법은 "예, 지금 지나간 시간에 기억의 특정 위치를 읽을 수 있습니다"라고 말하는 것 외에는 거의 유용하지 않습니다.

간단히 말해서,하지 마십시오;)

Raymond Chen은이 주제에 대한 블로그 게시물 : http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@Tim, C ++에서는 그렇게 할 수있는 방법이 없습니다.
JaredPar

6
"유효한 포인터"를 "접근 위반 / 세그 폴트를 유발하지 않음"으로 정의하면 "정답"일뿐입니다. 저는 이것을 "사용하려는 목적에 할당 된 의미있는 데이터에 대한 포인트"로 정의하고 싶습니다. 나는 그것이 포인터 유효성의 더 나은 정의라고 주장하고 싶습니다 ...;)
jalf

포인터가 유효하더라도이 방법으로 확인할 수 없습니다. thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sleep (1); p 삭제; ...}
Christopher

2
@Christopher, 매우 사실입니다. 나는 "나는 지금 지나간 시간에 기억의 특정 장소를 읽을 수있다"라고 말 했어야했다
JaredPar

@JaredPar : 정말 나쁜 제안입니다. 가드 페이지를 트리거 할 수 있으므로 스택이 나중에 확장되지 않거나 똑같이 좋은 것입니다.
Deduplicator

16

AFAIK 방법이 없습니다. 메모리를 해제 한 후 항상 포인터를 NULL로 설정하여 이러한 상황을 피해야합니다.


4
포인터를 null로 설정하면 잘못된 보안 감각을 제외하고는 아무것도 제공하지 않습니다.

그것은 사실이 아닙니다. 특히 C ++에서는 null을 확인하여 멤버 개체를 삭제할지 여부를 결정할 수 있습니다. 또한 C ++에서는 널 포인터를 삭제하는 것이 유효하므로 소멸자에서 객체를 무조건 삭제하는 것이 일반적입니다.
Ferdinand Beyer

4
int * p = 새로운 int (0); int * p2 = p; p 삭제; p = NULL; p2 삭제; // crash

1
zabzonk 및 ?? 그가 말한 것은 널 포인터를 삭제할 수 있다는 것입니다. p2는 널 포인터가 아니지만 유효하지 않은 포인터입니다. 이전에 null로 설정해야합니다.
Johannes Schaub-litb

2
가리키는 메모리에 대한 별칭이 있으면 그중 하나만 NULL로 설정되고 다른 별칭은 주위에 매달려 있습니다.
jdehaan

7

에 살펴보세요 질문을. 또한 스마트 포인터를 살펴보십시오 .


7

이 스레드의 답변에 대해서는 다음과 같습니다.

Windows의 경우 IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr ().

내 조언은 그들로부터 멀리 떨어져 있다는 것입니다. 누군가 이미 이것을 게시했습니다 : http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

같은 주제와 같은 저자 (제 생각에)에 대한 또 다른 게시물은 다음과 같습니다. http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ( "IsBadXxxPtr은 실제로 CrashProgramRandomly라고해야합니다. ").

API 사용자가 잘못된 데이터를 보내면 충돌이 발생하도록합니다. 문제가 전달 된 데이터가 나중에까지 사용되지 않고 원인을 찾기 어렵게 만드는 경우 문자열 등이 입력시 기록되는 디버그 모드를 추가합니다. 그들이 나쁘다면 그것은 명백 할 것입니다 (그리고 아마도 충돌 할 것입니다). 자주 발생하는 경우 API를 프로세스 외부로 이동하여 기본 프로세스 대신 API 프로세스를 중단시키는 것이 좋습니다.


아마도 또 다른 방법은 _CrtIsValidHeapPointer 를 사용하는 입니다. 이 함수는 포인터가 유효하면 TRUE를 반환하고 포인터가 해제되면 예외를 throw합니다. 설명 된대로이 함수는 디버그 CRT에서만 사용할 수 있습니다.
Crend King 2011 년

6

첫째, 고의로 충돌을 일으키려는 발신자로부터 자신을 보호하려고 노력하는 데 아무런 의미가 없습니다. 그들은 잘못된 포인터를 통해 접근을 시도함으로써 쉽게 이것을 할 수 있습니다. 다른 많은 방법이 있습니다. 메모리 나 스택을 덮어 쓸 수 있습니다. 이런 종류의 일로부터 보호해야하는 경우 통신을 위해 소켓이나 다른 IPC를 사용하여 별도의 프로세스에서 실행해야합니다.

우리는 파트너 / 고객 / 사용자가 기능을 확장 할 수있는 많은 소프트웨어를 작성합니다. 필연적으로 모든 버그가 먼저보고되므로 플러그인 코드에 문제가 있음을 쉽게 보여줄 수 있으면 유용합니다. 또한 보안 문제가 있으며 일부 사용자는 다른 사용자보다 더 신뢰할 수 있습니다.

성능 / 처리량 요구 사항 및 신뢰성에 따라 여러 가지 방법을 사용합니다. 가장 선호하는 항목 :

  • 소켓을 사용하는 별도의 프로세스 (종종 데이터를 텍스트로 전달).

  • 공유 메모리를 사용하는 별도의 프로세스 (대량의 데이터를 전달할 경우).

  • 동일한 프로세스는 메시지 대기열을 통해 스레드를 분리합니다 (짧은 메시지가 자주 발생하는 경우).

  • 동일한 프로세스는 메모리 풀에서 할당 된 모든 전달 된 데이터 스레드를 분리합니다.

  • 직접 프로 시저 호출을 통한 동일한 프로세스-메모리 풀에서 할당 된 모든 전달 된 데이터.

제 3 자 소프트웨어를 다룰 때, 특히 플러그인 / 라이브러리가 소스 코드가 아닌 바이너리로 제공되는 경우 사용자가하려는 작업에 절대로 의존하지 않습니다.

메모리 풀의 사용은 대부분의 상황에서 매우 쉽고 비효율적 일 필요는 없습니다. 처음에 데이터를 할당하는 경우 할당 한 값에 대해 포인터를 확인하는 것은 간단합니다. 할당 된 길이를 저장하고 데이터 앞뒤에 "매직"값을 추가하여 유효한 데이터 유형과 데이터 오버런을 확인할 수도 있습니다.


5

Unix에서는 다음과 같이 포인터 검사를 수행하고 EFAULT를 반환하는 커널 시스템 호출을 사용할 수 있어야합니다.

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>

bool isPointerBad( void * p )
{
   int fh = open( p, 0, 0 );
   int e = errno;

   if ( -1 == fh && e == EFAULT )
   {
      printf( "bad pointer: %p\n", p );
      return true;
   }
   else if ( fh != -1 )
   {
      close( fh );
   }

   printf( "good pointer: %p\n", p );
   return false;
}

int main()
{
   int good = 4;
   isPointerBad( (void *)3 );
   isPointerBad( &good );
   isPointerBad( "/tmp/blah" );

   return 0;
}

반환 :

bad pointer: 0x3
good pointer: 0x7fff375fd49c
good pointer: 0x400793

open () [아마도 액세스]보다 더 나은 syscall이있을 것입니다. 이것이 실제 파일 생성 코드 경로로 이어질 가능성이 있고 후속 닫기 요구 사항이 될 수 있기 때문입니다.


1
이것은 훌륭한 해킹입니다. 특히 부작용이 없다는 것을 보장 할 수 있다면 메모리 범위를 검증하기 위해 다른 시스템 호출에 대한 조언을보고 싶습니다. 버퍼가 읽을 수있는 메모리에 있는지 테스트하기 위해 / dev / null에 쓰기 위해 파일 설명자를 열어 둘 수 있지만 더 간단한 해결책이있을 수 있습니다. 내가 찾을 수있는 최선의 방법은 symlink (ptr, "")로 잘못된 주소에서는 errno를 14로, 좋은 주소에서는 2로 설정하지만 커널 변경은 확인 순서를 바꿀 수 있습니다.
Preston

1
@Preston DB2에서는 unistd.h의 access ()를 사용했다고 생각합니다. 조금 덜 모호하기 때문에 위의 open ()을 사용했지만 사용할 수있는 시스템 호출이 많이 있다는 것이 맞을 것입니다. Windows에는 명시 적 포인터 검사 API가 있었지만 스레드로부터 안전하지 않은 것으로 판명되었습니다 (SEH를 사용하여 메모리 범위의 경계를 작성하고 복원하려고 시도했습니다.)
Peeter Joot

4

나는 거의 동일한 위치에 있기 때문에 귀하의 질문에 많은 동정심을 가지고 있습니다. 나는 많은 응답이 말하는 것에 감사하고 그것들은 정확합니다. 포인터 제공하는 루틴 은 유효한 포인터를 제공 해야 합니다. 내 경우에는, 그들이 포인터를 손상 수 있었다는 것을 거의 상상할 수있다 - 그러나이 경우 관리, 내 소프트웨어가 될 것이라고 비난을받을 것이다 충돌 및 ME :-(

내 요구 사항은 세분화 오류 후에도 계속하는 것이 아닙니다. 위험 할 수 있습니다. 고객이 나를 비난하는 대신 코드를 수정할 수 있도록 종료하기 전에 발생한 일을 고객에게보고하고 싶습니다.

이것이 내가 그것을 (Windows에서) 찾은 방법입니다 : http://www.cplusplus.com/reference/clibrary/csignal/signal/

시놉시스를 제공하려면 :

#include <signal.h>

using namespace std;

void terminate(int param)
/// Function executed if a segmentation fault is encountered during the cast to an instance.
{
  cerr << "\nThe function received a corrupted reference - please check the user-supplied  dll.\n";
  cerr << "Terminating program...\n";
  exit(1);
}

...
void MyFunction()
{
    void (*previous_sigsegv_function)(int);
    previous_sigsegv_function = signal(SIGSEGV, terminate);

    <-- insert risky stuff here -->

    signal(SIGSEGV, previous_sigsegv_function);
}

이제이 나타납니다 (이 에러 메시지를 출력하고 프로그램을 종료) 나는 희망으로 행동하는 -하지만 누군가가 결함을 발견 할 수 있다면, 알려 주시기 바랍니다!


를 사용하지 마십시오. exit()RAII를 우회하므로 리소스 누수가 발생할 수 있습니다.
Sebastian Mach

흥미 롭습니다.이 상황에서 깔끔하게 종료하는 또 다른 방법이 있습니까? 그리고 exit 문이 이렇게 할 때 유일한 문제입니까? 나는 "-1"을 획득했다는 것을 알아 챘다-그것은 단지 '출구'때문인가?
Mike Sadler

죄송합니다. 매우 예외적 인 상황이라는 것을 알고 있습니다. 방금보고 exit()휴대용 C ++ 알람 벨이 울리기 시작했습니다. 프로그램이 어쨌든 종료되는이 Linux 특정 상황에서는 괜찮을 것입니다.
Sebastian Mach

1
signal (2)는 휴대용이 아닙니다. sigaction (2)을 사용하십시오. man 2 signalLinux에는 이유를 설명하는 단락이 있습니다.
rptb1

1
이 상황에서는 사후 문제를 진단하는 데 사용할 수있는 일종의 디버깅 역 추적을 생성 할 가능성이 높기 때문에 보통 exit (3) 대신 abort (3)를 호출합니다. 대부분의 Unixen에서 abort (3)는 코어를 덤프하고 (코어 덤프가 허용되는 경우) Windows에서는 설치된 경우 디버거를 실행하도록 제안합니다.
rptb1 2015-10-22

2

C ++에는 일반적인 경우로서 포인터의 유효성을 테스트 할 수있는 조항이 없습니다. NULL (0x00000000)이 나쁘다고 가정 할 수 있으며, 다양한 컴파일러와 라이브러리는 디버깅을 더 쉽게하기 위해 여기 저기에서 "특수 값"을 사용하는 것을 좋아합니다 (예를 들어 Visual Studio에서 포인터가 0xCECECECE로 표시되는 것을 본 경우 나는 뭔가 잘못했다) 그러나 진실은 포인터가 단지 메모리에 대한 인덱스이기 때문에 그것이 "올바른"인덱스인지 포인터를 보는 것만으로는 거의 불가능하다는 것입니다.

가리키는 객체가 원하는 유형인지 확인하기 위해 dynamic_cast 및 RTTI로 수행 할 수있는 다양한 트릭이 있지만, 모두 처음부터 유효한 것을 가리켜 야합니다.

프로그램이 "유효하지 않은"포인터를 감지 할 수 있는지 확인하려면 내 조언은 다음과 같습니다. 선언하는 모든 포인터를 생성 즉시 NULL 또는 유효한 주소로 설정하고 가리키는 메모리를 해제 한 후 즉시 NULL로 설정합니다. 이 관행에 부지런한 경우 NULL을 확인하는 것만으로도 충분합니다.


C ++ (또는 C)의 널 포인터 상수는 상수 정수 0으로 표시됩니다. 많은 구현에서 모든 이진 0을 사용하여 표현하지만 신뢰할 수있는 것은 아닙니다.
David Thornley

2

이를 수행하는 이식 가능한 방법은 없으며 특정 플랫폼에 대해 수행하는 것은 어렵거나 불가능할 수 있습니다. 어떤 경우에도 이러한 검사에 의존하는 코드를 작성해서는 안됩니다. 처음부터 포인터가 잘못된 값을 사용하도록하지 마십시오.


2

사용 전후에 포인터를 NULL로 설정하는 것은 좋은 기술입니다. 예를 들어 (문자열) 클래스 내에서 포인터를 관리하는 경우 C ++에서 쉽게 수행 할 수 있습니다.

class SomeClass
{
public:
    SomeClass();
    ~SomeClass();

    void SetText( const char *text);
    char *GetText() const { return MyText; }
    void Clear();

private:
    char * MyText;
};


SomeClass::SomeClass()
{
    MyText = NULL;
}


SomeClass::~SomeClass()
{
    Clear();
}

void SomeClass::Clear()
{
    if (MyText)
        free( MyText);

    MyText = NULL;
}



void SomeClass::Settext( const char *text)
{
    Clear();

    MyText = malloc( strlen(text));

    if (MyText)
        strcpy( MyText, text);
}

업데이트 된 질문은 물론 내 대답이 잘못되었습니다 (또는 적어도 다른 질문에 대한 대답). 나는 기본적으로 그들이 api를 남용하면 충돌을 허용한다는 답변에 동의합니다. 당신은 ... 사람들이 망치로 엄지 손가락에 자신을 치는 중지 할 수 없습니다
팀 링에게

2

퍼블릭 API에서 임의의 포인터를 입력 매개 변수로 받아들이는 것은 좋은 정책이 아닙니다. 정수, 문자열 또는 구조체와 같은 "일반 데이터"유형을 갖는 것이 더 좋습니다 (물론 일반 데이터가 포함 된 고전적 구조체를 의미합니다. 공식적으로 모든 것이 구조체가 될 수 있음).

왜? 다른 사람들이 말하는 것처럼 유효한 포인터를 받았는지 아니면 정크를 가리키는 포인터를 받았는지 알 수있는 표준 방법이 없기 때문입니다.

하지만 때로는 선택의 여지가 없습니다. API는 포인터를 받아야합니다.

이 경우 좋은 포인터를 전달하는 것은 호출자의 의무입니다. NULL은 값으로 허용 될 수 있지만 정크에 대한 포인터는 아닙니다.

어떤 식 으로든 다시 확인할 수 있습니까? 글쎄, 그런 경우에 내가 한 일은 포인터가 가리키는 유형에 대한 불변을 정의하고 얻을 때 (디버그 모드에서) 호출하는 것이 었습니다. 적어도 불변이 실패 (또는 충돌)하면 잘못된 값이 전달되었음을 알 수 있습니다.

// API that does not allow NULL
void PublicApiFunction1(Person* in_person)
{
  assert(in_person != NULL);
  assert(in_person->Invariant());

  // Actual code...
}

// API that allows NULL
void PublicApiFunction2(Person* in_person)
{
  assert(in_person == NULL || in_person->Invariant());

  // Actual code (must keep in mind that in_person may be NULL)
}

re : "pass a plain data type ... like a string"그러나 C ++에서 문자열은 대부분의 경우 문자 (char *) 또는 (const char *)에 대한 포인터로 전달되므로 포인터 전달로 돌아갑니다. 그리고 귀하의 예제는 in_person을 포인터가 아닌 참조로 전달하므로 비교 (in_person! = NULL)는 Person 클래스에 정의 된 일부 개체 / 포인터 비교가 있음을 의미합니다.
Jesse Chisholm 2014

@JesseChisholm 문자열이란 문자열, 즉 std :: string을 의미합니다. 문자열을 저장하거나 전달하는 방법으로 char * 사용을 권장하지 않습니다. 그러지 마.
Daniel Daranas 2014-04-24

@JesseChisholm 어떤 이유로 5 년 전에이 질문에 답했을 때 실수를했습니다. 분명히 Person &가 NULL인지 확인하는 것은 의미가 없습니다. 그것은 컴파일조차하지 않을 것입니다. 나는 참조가 아닌 포인터를 사용하는 것을 의미했습니다. 지금 고쳤습니다.
Daniel Daranas 2014

1

다른 사람들이 말했듯이 잘못된 포인터를 안정적으로 감지 할 수 없습니다. 유효하지 않은 포인터가 취할 수있는 몇 가지 형식을 고려하십시오.

널 포인터가있을 수 있습니다. 그것은 당신이 쉽게 확인하고 뭔가를 할 수있는 것입니다.

유효한 메모리 외부에 대한 포인터를 가질 수 있습니다. 유효한 메모리를 구성하는 것은 시스템의 런타임 환경이 주소 공간을 설정하는 방법에 따라 다릅니다. Unix 시스템에서는 일반적으로 0에서 시작하여 많은 수의 메가 바이트로 이동하는 가상 주소 공간입니다. 임베디드 시스템에서는 매우 작을 수 있습니다. 어떤 경우에도 0에서 시작하지 않을 수 있습니다. 앱이 감독자 모드 또는 이와 동등한 모드에서 실행되는 경우 포인터가 실제 주소를 참조 할 수 있으며, 실제 주소는 실제 메모리로 백업되거나 백업되지 않을 수 있습니다.

데이터 세그먼트, bss, 스택 또는 힙 내부에서도 유효한 메모리 내부 어딘가에 대한 포인터를 가질 수 있지만 유효한 개체를 가리키는 것은 아닙니다. 이것의 변형은 개체에 문제가 발생하기 전에 유효한 개체를 가리키는 데 사용되는 포인터입니다. 이 맥락에서 나쁜 것은 할당 해제, 메모리 손상 또는 포인터 손상을 포함합니다.

참조되는 항목에 대해 잘못된 정렬이있는 포인터와 같이 평면이 아닌 잘못된 포인터가있을 수 있습니다.

세그먼트 / 오프셋 기반 아키텍처 및 기타 이상한 포인터 구현을 고려하면 문제가 더욱 악화됩니다. 이런 종류의 일은 일반적으로 좋은 컴파일러와 현명한 유형의 사용으로 인해 개발자에게 숨겨져 있지만 베일을 뚫고 운영 체제와 컴파일러 개발자를 능가하려는 경우 할 수 있지만 일반적인 방법은 없습니다. 발생할 수있는 모든 문제를 처리 할 수 ​​있습니다.

최선의 방법은 충돌을 허용하고 좋은 진단 정보를 제공하는 것입니다.


re : "좋은 진단 정보를 내놓으십시오", 문제가 있습니다. 포인터 유효성을 확인할 수 없기 때문에 소란 스러워야하는 정보는 최소화됩니다. "여기에서 예외가 발생했습니다."가 전부일 수 있습니다. 전체 호출 스택은 좋지만 대부분의 C ++ 런타임 라이브러리가 제공하는 것보다 더 나은 프레임 워크가 필요합니다.
Jesse Chisholm 2014


1

일반적으로 불가능합니다. 특히 불쾌한 경우가 있습니다.

struct Point2d {
    int x;
    int y;
};

struct Point3d {
    int x;
    int y;
    int z;
};

void dump(Point3 *p)
{
    printf("[%d %d %d]\n", p->x, p->y, p->z);
}

Point2d points[2] = { {0, 1}, {2, 3} };
Point3d *p3 = reinterpret_cast<Point3d *>(&points[0]);
dump(p3);

많은 플랫폼에서 다음과 같이 출력됩니다.

[0 1 2]

런타임 시스템이 메모리 비트를 잘못 해석하도록 강요하지만,이 경우 비트가 모두 의미가 있기 때문에 충돌하지 않을 것입니다. 이것은 (와 C 스타일의 다형성 봐 언어의 설계의 일부입니다 struct inaddr, inaddr_in, inaddr_in6당신이 안정적으로 모든 플랫폼에 대해 보호 할 수 있도록).


1

위의 기사에서 얼마나 많은 오해의 소지가있는 정보를 읽을 수 있는지 믿을 수 없습니다.

그리고 Microsoft msdn 문서에서도 IsBadPtr은 금지되었다고 주장됩니다. 오 글쎄-나는 충돌보다는 작동하는 응용 프로그램을 선호합니다. 용어 작업이 잘못 작동하더라도 (최종 사용자가 응용 프로그램을 계속할 수있는 한).

인터넷 검색을 통해 Windows에 대한 유용한 예를 찾지 못했습니다. 32 비트 앱에 대한 솔루션을 찾았습니다.

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2Fsrc%2FdrvCppLib%2Frtti.cpp&obid=58895&obtid=2&ovid = 2

하지만 64 비트 앱도 지원해야하므로이 솔루션이 작동하지 않았습니다.

하지만 와인의 소스 코드를 모아서 64 비트 앱에서도 작동하는 비슷한 종류의 코드를 만들었습니다. 여기에 코드를 첨부하세요.

#include <typeinfo.h>   

typedef void (*v_table_ptr)();   

typedef struct _cpp_object   
{   
    v_table_ptr*    vtable;   
} cpp_object;   



#ifndef _WIN64
typedef struct _rtti_object_locator
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    const type_info *type_descriptor;
    //const rtti_object_hierarchy *type_hierarchy;
} rtti_object_locator;
#else

typedef struct
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    unsigned int type_descriptor;
    unsigned int type_hierarchy;
    unsigned int object_locator;
} rtti_object_locator;  

#endif

/* Get type info from an object (internal) */  
static const rtti_object_locator* RTTI_GetObjectLocator(void* inptr)  
{   
    cpp_object* cppobj = (cpp_object*) inptr;  
    const rtti_object_locator* obj_locator = 0;   

    if (!IsBadReadPtr(cppobj, sizeof(void*)) &&   
        !IsBadReadPtr(cppobj->vtable - 1, sizeof(void*)) &&   
        !IsBadReadPtr((void*)cppobj->vtable[-1], sizeof(rtti_object_locator)))  
    {  
        obj_locator = (rtti_object_locator*) cppobj->vtable[-1];  
    }  

    return obj_locator;  
}  

그리고 다음 코드는 포인터가 유효한지 여부를 감지 할 수 있으므로 NULL 검사를 추가해야합니다.

    CTest* t = new CTest();
    //t = (CTest*) 0;
    //t = (CTest*) 0x12345678;

    const rtti_object_locator* ptr = RTTI_GetObjectLocator(t);  

#ifdef _WIN64
    char *base = ptr->signature == 0 ? (char*)RtlPcToFileHeader((void*)ptr, (void**)&base) : (char*)ptr - ptr->object_locator;
    const type_info *td = (const type_info*)(base + ptr->type_descriptor);
#else
    const type_info *td = ptr->type_descriptor;
#endif
    const char* n =td->name();

이것은 포인터에서 클래스 이름을 가져옵니다-귀하의 요구에 충분해야한다고 생각합니다.

내가 여전히 두려워하는 한 가지는 포인터 검사의 성능입니다. 위의 코드 스 니펫에는 이미 3-4 개의 API 호출이 수행되고 있습니다. 시간이 중요한 애플리케이션에서는 과도 할 수 있습니다.

예를 들어 C # / 관리되는 C ++ 호출과 비교하여 누군가가 포인터 검사의 오버 헤드를 측정 할 수 있다면 좋을 것입니다.


1

예를 들어 문자열 포인터 문자열이 유효한지 확인하려는 경우 write (fd, buf, szie) syscall을 사용하여 마법을 수행 할 수 있습니다. 테스트를 위해 만든 파일과 테스트중인 문자열을 가리키는 buf는 포인터가 유효하지 않은 경우 write ()가 -1을 반환하고 errno를 EFAULT로 설정하여 buf가 액세스 가능한 주소 공간 밖에 있음을 나타냅니다.


1

다음은 Windows에서 작동합니다 (누군가가 전에 제안했습니다).

 static void copy(void * target, const void* source, int size)
 {
     __try
     {
         CopyMemory(target, source, size);
     }
     __except(EXCEPTION_EXECUTE_HANDLER)
     {
         doSomething(--whatever--);
     }
 }

함수는 일부 클래스의 정적, 독립형 또는 정적 메서드 여야합니다. 읽기 전용으로 테스트하려면 로컬 버퍼에 데이터를 복사하십시오. 내용을 수정하지 않고 쓰기 테스트를하려면 덮어 씁니다. 첫 번째 / 마지막 주소 만 테스트 할 수 있습니다. 포인터가 유효하지 않으면 제어가 'doSomething'에 전달 된 다음 대괄호 외부에 전달됩니다. CString과 같이 소멸자가 필요한 것은 사용하지 마십시오.


1

Windows에서는 다음 코드를 사용합니다.

void * G_pPointer = NULL;
const char * G_szPointerName = NULL;
void CheckPointerIternal()
{
    char cTest = *((char *)G_pPointer);
}
bool CheckPointerIternalExt()
{
    bool bRet = false;

    __try
    {
        CheckPointerIternal();
        bRet = true;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    return  bRet;
}
void CheckPointer(void * A_pPointer, const char * A_szPointerName)
{
    G_pPointer = A_pPointer;
    G_szPointerName = A_szPointerName;
    if (!CheckPointerIternalExt())
        throw std::runtime_error("Invalid pointer " + std::string(G_szPointerName) + "!");
}

용법:

unsigned long * pTest = (unsigned long *) 0x12345;
CheckPointer(pTest, "pTest"); //throws exception

0

Windows의 경우 IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr ().
이것들은 블록의 길이에 비례하여 시간이 걸리므로 온 전성 확인을 위해 시작 주소 만 확인합니다.


3
이러한 방법은 작동하지 않으므로 피해야합니다. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx
JaredPar

때로는 작동하지 않는 문제에 대한 해결 방법 일 수 있습니다. stackoverflow.com/questions/496034/…
ChrisW

0

다양한 라이브러리가 참조되지 않은 메모리 등을 확인하는 방법을 사용하는 것을 보았습니다. 포인터를 추적하는 로직이있는 메모리 할당 및 할당 해제 방법 (malloc / free)을 단순히 "무시"한다고 생각합니다. 나는 이것이 당신의 사용 사례에 과잉이라고 생각하지만 그것을하는 한 가지 방법이 될 것입니다.


불행히도 스택 할당 객체에는 도움이되지 않습니다.

0

기술적으로는 operator new (및 delete )를 재정의 하고 할당 된 모든 메모리에 대한 정보를 수집 할 수 있으므로 힙 메모리가 유효한지 확인하는 방법을 가질 수 있습니다. 그러나:

  1. 포인터가 스택 ()에 할당되었는지 확인하는 방법이 여전히 필요합니다.

  2. '유효한'포인터가 무엇인지 정의해야합니다.

a) 해당 주소의 메모리가 할당됩니다.

b) 해당 주소의 메모리 는 객체의 시작 주소입니다 (예 : 거대한 배열의 중간에 있지 않은 주소).

c) 해당 주소의 메모리 는 예상 유형 의 객체의 시작 주소입니다.

결론 : 문제의 접근 방식은 C ++ 방식이 아닙니다. 함수가 유효한 포인터를 받도록하는 몇 가지 규칙을 정의해야합니다.



0

승인 된 답변에 대한 부록 :

포인터가 0, 1 및 -1의 세 값만 보유 할 수 있다고 가정합니다. 여기서 1은 유효한 포인터, -1은 유효하지 않은 포인터, 0은 유효하지 않은 포인터를 나타냅니다. 포인터 NULL이고 모든 값이 같을 확률은 얼마입니까? 1/3. 이제 유효한 케이스를 꺼내서 모든 유효하지 않은 케이스에 대해 50:50 비율로 모든 오류를 포착합니다. 좋아 보이지? 4 바이트 포인터에 대해이 크기를 조정하십시오. 2 ^ 32 또는 4294967294 가능한 값이 있습니다. 이 중 하나의 값만 정확하고 하나는 NULL이며 여전히 다른 잘못된 경우 4294967292가 남아 있습니다. 재 계산 : 유효하지 않은 케이스 (4294967292+ 1) 중 1 개에 대한 테스트가 있습니다. 대부분의 실제 목적에서 2.xe-10 또는 0의 확률입니다. 이것이 NULL 검사의 무익함입니다.


0

이 기능을 사용할 수있는 새 드라이버 (적어도 Linux에서는)는 작성하기가 그렇게 어렵지 않을 것입니다.

반면에 이와 같은 프로그램을 만드는 것은 어리석은 일입니다. 그런 일에 대해 정말로 구체적이고 단일 용도가 없다면 나는 그것을 권장하지 않을 것입니다. 일정한 포인터 유효성 검사를 통해로드 된 대규모 애플리케이션을 빌드하면 끔찍하게 느려질 수 있습니다.


0

이러한 방법은 작동하지 않으므로 피해야합니다. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx – JaredPar 2009 년 2 월 15 일 16:02

작동하지 않으면 다음 Windows 업데이트가 문제를 해결합니까? 개념 수준에서 작동하지 않으면 기능이 Windows API에서 완전히 제거 될 수 있습니다.

MSDN 문서는 그것들이 금지되어 있다고 주장하며, 그 이유는 아마도 추가 응용 프로그램 디자인의 결함 일 것입니다 (예를 들어, 일반적으로 전체 응용 프로그램의 디자인을 담당하는 경우 일반적으로 잘못된 포인터를 조용히 먹어서는 안 됨) 및 성능 / 시간 포인터 검사의.

그러나 일부 블로그 때문에 작동하지 않는다고 주장해서는 안됩니다. 내 테스트 응용 프로그램에서 작동하는지 확인했습니다.


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