Тестування покажчиків на валідність (C / C ++)


91

Чи є спосіб визначити (звичайно, програмно), чи вказаний покажчик є "дійсним"? Перевірити наявність NULL легко, але як щодо таких речей, як 0x00001234? При спробі розблокування цього виду вказівника відбувається виняток / збій.

Переважний крос-платформенний метод, але певний для платформи (для Windows та Linux) також є нормальним.

Оновлення для роз’яснення: проблема полягає не в застарілих / звільнених / неініціалізованих покажчиках; натомість я реалізую API, який приймає покажчики від абонента (наприклад, вказівник на рядок, дескриптор файлу тощо). Абонент може надіслати (цілеспрямовано або помилково) недійсне значення як покажчик. Як запобігти аварії?


Дивіться також stackoverflow.com/questions/496034 / ...
ChrisW

Я думаю, що найкращу позитивну відповідь на Linux дає Джордж Карретт. Якщо цього недостатньо, розгляньте можливість побудови таблиці символів функції в бібліотеці або навіть іншого рівня таблиці доступних бібліотек із власними таблицями функцій. Потім перевірте за цими точними таблицями. Звичайно, ці негативні відповіді також є правильними: ви насправді не можете бути впевнені на 100%, чи є покажчик функції дійсним чи ні, якщо ви не встановите багато додаткових обмежень для програми користувача.
minghua

Чи специфікація API насправді визначає таке зобов'язання, яке потрібно виконати впровадженням? До речі, я роблю вигляд, що вас не вважали, що ви і розробник, і дизайнер. Моя суть полягає в тому, що я не думаю, що API повинен вказати щось на зразок "У разі передачі невірного вказівника як аргументу, функція повинна обробляти проблему і повертати NULL.". API зобов'язується надавати послугу за належних умов використання, а не за допомогою хакерів. Тим не менше, не шкодить бути трохи дурним. Використання посилання робить такі випадки менш розповсюджуючими. :)
Понірос,

Відповіді:


75

Оновлення для роз’яснення: проблема полягає не в застарілих, звільнених чи неініціалізованих покажчиках; натомість я реалізую API, який приймає покажчики від абонента (наприклад, вказівник на рядок, дескриптор файлу тощо). Абонент може надіслати (цілеспрямовано або помилково) недійсне значення як покажчик. Як запобігти аварії?

Ви не можете зробити цю перевірку. Ви просто не можете перевірити, чи є покажчик "дійсним". Ви повинні вірити, що коли люди використовують функцію, яка приймає покажчик, ці люди знають, що вони роблять. Якщо вони передають вам 0x4211 як значення вказівника, то вам доведеться довіряти, що він вказує на адресу 0x4211. І якщо вони "випадково" потраплять на об'єкт, то навіть якщо ви використовуєте якусь страшну функцію операційної системи (IsValidPtr або іншу), ви все одно проскакуєте в помилку і не швидко вийдете з ладу.

Почніть використовувати нульові вказівники для сигналізації такого роду речей і повідомте користувачеві вашої бібліотеки, що вони не повинні використовувати вказівники, якщо вони, як правило, випадково передають недійсні вказівники, серйозно :)


Це, мабуть, правильна відповідь, але я думаю, що проста функція, яка перевіряє загальні місця з шестнадцятковим запам'ятовуванням, була б корисною для загальної налагодження ... Зараз у мене є покажчик, який іноді вказує на 0xfeeefeee, і якби я мав просту функцію, яку я міг би використовувати для перцю тверджень навколо Це значно полегшило б знайти винуватця ... РЕДАКТУВАТИ: Хоча, мабуть, не складно написати такого
Квант

Проблема в @quant полягає в тому, що деякі коди C та C ++ можуть виконувати арифметику покажчиків на недійсній адресі без перевірки (на основі принципу сміття, сміття) і, отже, передаватиме "арифметично модифікований" покажчик з однієї з цих свердловин -невідомі недійсні адреси. Поширеними випадками є пошук методу з неіснуючої vtable на основі недійсної адреси об'єкта або одного неправильного типу, або просто читання полів від вказівника до структури, яка не вказує на одну.
rwong

Це в основному означає, що ви можете брати індекси масивів лише із зовнішнього світу. API, який повинен захищатися від абонента, просто не може мати вказівників в інтерфейсі. Однак все-таки було б добре мати макроси, які можна використовувати у твердженнях про дійсність покажчиків (які ви обов’язково повинні мати внутрішньо). Якщо вказівник гарантовано вказує всередині масиву, початкова точка та довжина якого відомі, це можна перевірити явно. Краще померти через порушення твердження (задокументована помилка), аніж дереф (недокументована помилка).
Роб

34

Ось три простих способи, як програма C під Linux може проаналізувати стан пам'яті, в якій вона працює, і чому питання має відповідні складні відповіді в деяких контекстах.

  1. Після виклику getpagesize () і округлення покажчика до межі сторінки, ви можете зателефонувати mincore (), щоб з'ясувати, чи є сторінка дійсною і чи є вона частиною робочого набору процесу. Зауважте, що для цього потрібні певні ресурси ядра, тому вам слід порівняти його та визначити, чи дійсно виклик цієї функції доречний у вашому api. Якщо ваш api збирається обробляти переривання або читати з послідовних портів у пам'ять, доцільно викликати це, щоб уникнути непередбачуваної поведінки.
  2. Зателефонувавши stat (), щоб визначити, чи є каталог / proc / self доступний, ви можете виконати пошук та / proc / self / maps, щоб знайти інформацію про регіон, в якому знаходиться вказівник. Вивчіть man-сторінку для proc, псевдо-файлової системи інформації про процес. Очевидно, що це відносно дорого, але, можливо, вам вдасться уникнути кешування результату синтаксичного аналізу в масив, який ви можете ефективно шукати, використовуючи двійковий пошук. Також розгляньте / proc / self / smaps. Якщо ваш api призначений для високопродуктивних обчислень, тоді програма захоче дізнатись про / proc / self / numa, який задокументовано на сторінці користувача для numa, нерівномірної архітектури пам'яті.
  3. Виклик get_mempolicy (MPOL_F_ADDR) підходить для високопродуктивних обчислювальних api-робіт, де є кілька потоків виконання, і ви керуєте своєю роботою, щоб мати спорідненість до неоднорідної пам'яті, оскільки вона стосується ядер процесора та ресурсів сокета. Такий api, звичайно, також повідомляє вам, чи вказівник є дійсним.

Під Microsoft Windows існує функція QueryWorkingSetEx, яка задокументована в API статусу процесу (також в NUMA API). Як наслідок складного програмування NUMA API ця функція також дозволить вам виконувати прості роботи "тестування покажчиків на валідність (C / C ++)", оскільки такі навряд чи будуть застарілими принаймні на 15 років.


13
Перша відповідь, яка не намагається бути моральною щодо самого питання і насправді ідеально відповідає на нього. Люди іноді не усвідомлюють, що дійсно потрібен такий підхід налагодження для пошуку помилок, наприклад, у сторонніх бібліотеках або в застарілому коді, оскільки навіть valgrind знаходить дикі вказівники лише під час фактичного доступу до них, а не, наприклад, якщо ви хочете регулярно перевіряти вказівники на валідність у таблиці кеш-пам’яті, перезаписаній з іншого місця у вашому коді ...
lumpidu

Це має бути прийнятою відповіддю. Я зробив це одночасно на не-Linux платформі. По суті, це розкриття інформації про процес самому процесу. З цього аспекту, схоже, Windows робить кращу роботу, ніж Linux, виставляючи більш значущу інформацію через API статусу процесу.
minghua

31

Запобігання аварійному завершенню, спричиненому надсиланням абонентом недійсного вказівника, є хорошим способом усунення тихих помилок, які важко знайти.

Чи не краще для програміста, який використовує ваш API, отримати чітке повідомлення про те, що його код є фальшивим, розбивши його, а не приховуючи?


8
Однак у деяких випадках перевірка на наявність неправильного вказівника негайно, коли API викликається, полягає в тому, як ви рано провалюєтеся. Наприклад, що, якщо API зберігає вказівник у структурі даних, де він буде визначений лише пізніше? Тоді передача API поганого вказівника спричинить збій у якийсь випадковий пізній момент. У цьому випадку було б краще провалитися раніше, при виклику API, де спочатку було введено неправильне значення.
peterflynn

28

У Win32 / 64 є спосіб зробити це. Спробуйте прочитати вказівник і зафіксувати результуюче виняток SEH, який буде викинуто в разі відмови. Якщо він не кидає, то це дійсний покажчик.

Проблема цього методу полягає в тому, що він просто повертає, чи можете ви читати дані з покажчика чи ні. Це не гарантує безпеки типу або будь-якої кількості інших інваріантів. Загалом, цей метод корисний лише для того, щоб сказати "так, я можу прочитати це конкретне місце в пам'яті в той час, який минув".

Коротше, не робіть цього;)

Раймонд Чень має допис у блозі на цю тему: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@Tim, це неможливо зробити в C ++.
JaredPar

6
Це лише "правильна відповідь", якщо ви визначите "дійсний покажчик" як "не спричиняє порушення доступу / segfault". Я вважаю за краще визначити це як "вказує на значущі дані, виділені для цілі, яку ви збираєтеся використовувати". Я б стверджував, що це краще визначення дійсності покажчика ...;)
jalf

Навіть якщо вказівник є дійсним, перевірити це неможливо. Подумайте про thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sleep (1); видалити p; ...}
Крістофер

2
@Christopher, дуже правда. Я повинен був сказати: "Я можу прочитати це місце в пам'яті за час, що минув"
ДжаредПар

@JaredPar: Дійсно погана пропозиція. Може викликати захисну сторінку, тому стек не буде розширено пізніше або щось настільки ж приємне.
Дедулікатор

16

AFAIK ніяк. Вам слід спробувати уникнути цієї ситуації, завжди встановлюючи покажчики на NULL після звільнення пам'яті.


4
Встановлення покажчика в нуль не дає вам нічого, крім, можливо, помилкового відчуття безпеки.

Це не правда. Особливо в C ++ ви можете визначити, чи видаляти об'єкти-члени, перевіряючи наявність null. Також зауважте, що в C ++ допустимо видаляти нульові вказівники, тому безумовне видалення об’єктів у деструкторах є популярним.
Фердинанд Бейер

4
int * p = новий int (0); int * p2 = p; видалити p; p = НУЛЬ; видалити p2; // крах

1
забзонк, а ?? він сказав, що ви можете видалити нульовий покажчик. p2 не є нульовим покажчиком, але є недійсним покажчиком. вам потрібно встановити для нього нуль раніше.
Йоханнес Шауб - літ

2
Якщо у вас є псевдоніми на вказану пам’ять, лише одному з них буде встановлено значення NULL, інші псевдоніми звисають навколо.
jdehaan

7

Погляньте на це і це питання. Також зверніть увагу на розумні вказівники .


7

Щодо відповіді трохи в цій темі:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () для Windows.

Моя порада - триматися подалі від них, хтось уже опублікував цей: 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, якщо вказівник є дійсним, і видасть виняток, коли вказівник звільнено. Як задокументовано, ця функція доступна лише в CRT для налагодження.
Crend King

6

По-перше, я не бачу сенсу намагатися захистити себе від того, хто телефонує, навмисно намагаючись викликати аварію. Вони могли легко зробити це, спробувавши доступ через недійсний вказівник самі. Є багато інших способів - вони можуть просто перезаписати вашу пам’ять або стек. Якщо вам потрібно захистити від подібного роду речей, тоді вам потрібно запускати окремий процес, використовуючи сокети або інший IPC для зв'язку.

Ми пишемо досить багато програмного забезпечення, яке дозволяє партнерам / клієнтам / користувачам розширити функціональність. Неминуче спочатку нам повідомляють про будь-яку помилку, тому корисно мати можливість легко показати, що проблема полягає в коді плагіна. Крім того, є проблеми з безпекою, і деяким користувачам довіряють більше, ніж іншим.

Ми використовуємо низку різних методів залежно від вимог до продуктивності / пропускної здатності та надійності. З найбільш бажаних:

  • окремі процеси, використовуючи сокети (часто передаючи дані у вигляді тексту).

  • окремі процеси з використанням спільної пам'яті (якщо потрібно передавати великі обсяги даних).

  • той самий процес обробляє окремі потоки через чергу повідомлень (якщо часті короткі повідомлення).

  • той самий процес окремі потоки всі передані дані, виділені з пулу пам'яті.

  • той самий процес за допомогою прямого виклику процедури - всі передані дані, виділені з пулу пам'яті.

Ми намагаємось ніколи не вдаватися до того, що ви намагаєтесь робити, маючи справу зі стороннім програмним забезпеченням, особливо коли нам плагіни / бібліотека надаються як двійковий, а не як вихідний код.

Використання пулу пам'яті досить просто в більшості випадків і не повинно бути неефективним. Якщо ВИ розподіляєте дані в першу чергу, тоді тривіально перевіряти покажчики щодо виділених вами значень. Ви також можете зберегти призначену довжину та додати "магічні" значення до і після даних, щоб перевірити наявність дійсних типів даних та перевищення даних.


5

У Unix ви повинні мати можливість використовувати syscall ядра, яке виконує перевірку покажчиків і повертає 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

Можливо, є кращий syscall для використання, ніж open () [можливо, доступ], оскільки існує ймовірність, що це може призвести до фактичного створення кодового шляху до файлу та подальшої вимоги до закриття.


1
Це блискучий хак. Я хотів би побачити поради щодо різних системних викликів для перевірки діапазону пам’яті, особливо якщо вони можуть гарантувати відсутність побічних ефектів. Ви можете тримати дескриптор файлу відкритим для запису в / dev / null, щоб перевірити, чи є буфери в читабельній пам'яті, але, ймовірно, існують і більш прості рішення. Найкраще, що я можу знайти, це символьне посилання (ptr, ""), яке встановить для errno значення 14 на неправильну адресу або 2 на хорошу адресу, але зміни ядра можуть поміняти порядок перевірки.
Престон,

1
@Preston У DB2, я думаю, раніше ми використовували доступ unistd.h (). Я використовував open () вище, оскільки він трохи менш затьмарений, але ви, мабуть, маєте рацію, що існує багато можливих системних викликів. Раніше у Windows був явний API перевірки вказівників, але він виявився не безпечним для потоків (я думаю, що він використовував SEH, щоб спробувати написати, а потім відновити межі діапазону пам'яті.)
Peeter Joot

4

Я дуже симпатизую вашому питанню, оскільки сам я перебуваю майже в однаковому положенні. Я ціную те, що багато відповідей говорять, і вони правильні - процедура подання вказівника повинна забезпечувати дійсний вказівник. У моєму випадку майже немислимо, що вони могли пошкодити вказівник - але якби їм це вдалося, це МОЄ програмне забезпечення, яке аварійно завершує роботу, а МЕНЕ винні: --(

Моя вимога полягає не в тому, щоб я продовжував діяти після помилки сегментації - це було б небезпечно - я просто хочу повідомити про те, що сталося із замовником, перед тим, як припинити роботу, щоб вони могли виправити свій код, а не звинувачувати мене!

Ось як я це зробив (у 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 і, отже, може спричинити витік ресурсів.
Себастьян Мах

Цікаво - чи існує інший спосіб акуратно закінчити в цій ситуації? І чи є заява виходу єдиною проблемою, коли це робиться так? Я помічаю, що отримав "-1" - це просто через "вихід"?
Mike Sadler

На жаль, я розумію, що це для досить виняткової ситуації. Я щойно побачив, exit()і мій портативний дзвінок сигналізації C ++ почав дзвонити. У цій конкретній ситуації для Linux, де ваша програма все одно вийде, вибачте за шум, це повинно бути нормально.
Себастьян Мах

1
сигнал (2) не є портативним. Використовуйте сигакцію (2). man 2 signalна Linux є параграф, що пояснює, чому.
rptb1

1
У цій ситуації я зазвичай називаю аборт (3), а не вихід (3), оскільки це, швидше за все, створює певний зворотний трас налагодження, який ви можете використовувати для діагностики проблеми після забою. У більшості Unixen, abort (3) скине ядро ​​(якщо дозволені дампи ядра), а в Windows запропонує запустити налагоджувач, якщо він встановлений.
rptb1

2

У C ++ немає положень для перевірки дійсності вказівника як загального випадку. Очевидно, можна припустити, що NULL (0x00000000) поганий, і різні компілятори та бібліотеки люблять використовувати "спеціальні значення" тут і там, щоб полегшити налагодження (Наприклад, якщо я коли-небудь бачу, що вказівник відображається як 0xCECECECE у візуальній студії, я знаю Я зробив щось не так), але правда полягає в тому, що оскільки вказівник - це просто індекс в пам'яті, майже неможливо визначити, просто подивившись на вказівник, чи це "правильний" індекс.

Існують різні трюки, які ви можете зробити з dynamic_cast та RTTI, щоб переконатися, що об’єкт, на який вказують, має той тип, який ви хочете, але всі вони вимагають, щоб ви вказували на щось дійсне в першу чергу.

Якщо ви хочете переконатись, що ваша програма може виявити "недійсні" покажчики, тоді моя порада така: встановіть для кожного оголошеного вами покажчика значення NULL або дійсну адресу відразу після створення та встановіть для нього значення NULL відразу після звільнення пам'яті, на яку він вказує. Якщо ви старанно ставитеся до цієї практики, то перевірка на NULL - це все, що вам коли-небудь знадобиться.


Нульова константа вказівника в C ++ (або C, з цього приводу) представлена ​​постійним інтегральним нулем. Багато реалізацій використовують все двійкові нулі для його представлення, але це не те, на що можна розраховувати.
Девід Торнлі,

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: "передати звичайний тип даних ... як рядок". Але в C ++ рядки найчастіше передаються як вказівники на символи (char *) або (const char *), тож ви повертаєтеся до передавання покажчиків. І ваш приклад передає in_person як посилання, а не як вказівник, тому порівняння (in_person! = NULL) означає, що є порівняння об’єктів / покажчиків, визначених у класі Person.
Jesse Chisholm

@JesseChisholm Під рядком я мав на увазі рядок, тобто std :: string. Я жодним чином не рекомендую використовувати char * як спосіб зберігати рядки або передавати їх. Не роби цього.
Даніель Даранас,

@JesseChisholm Чомусь я помилився, коли відповів на це запитання п’ять років тому. Зрозуміло, не має сенсу перевіряти, чи є Особа & ПІЛЮЩИМ. Це навіть не скомпілювати. Я мав на увазі використовувати вказівники, а не посилання. Я це виправив зараз.
Даніель Даранас,

1

Як казали інші, ви не можете надійно виявити невірний вказівник. Розглянемо деякі форми, які може приймати недійсний вказівник:

Ви можете мати нульовий покажчик. Це той, за яким ви можете легко перевірити та щось зробити.

Ви можете мати вказівник на десь поза дійсною пам’яттю. Що становить дійсну пам'ять, залежить від того, як середовище виконання вашої системи встановлює адресний простір. У системах Unix це, як правило, віртуальний адресний простір, починаючи з 0 і переходячи до деякої великої кількості мегабайт. На вбудованих системах це може бути досить мало. У будь-якому випадку це може починатися не з 0. Якщо ваш додаток працює в режимі супервізора або в еквіваленті, тоді вказівник може посилатися на реальну адресу, яка може бути або не підкріплена реальною пам'яттю.

Ви можете мати вказівник на десь у вашій дійсній пам’яті, навіть у вашому сегменті даних, bss, стеку або купі, але не вказуючи на дійсний об’єкт. Варіант цього - вказівник, який вказував на дійсний об’єкт до того, як із об’єктом сталося щось погане. Погані речі в цьому контексті включають вивільнення, пошкодження пам’яті чи пошкодження покажчика.

У вас може бути плоский незаконний вказівник, наприклад, вказівник з незаконним вирівнюванням для речі, на яку посилається.

Проблема стає ще гіршою, якщо врахувати архітектури на основі сегментів / зсувів та інші реалізації непарних покажчиків. Такі речі зазвичай приховуються від розробника хорошими компіляторами та розумним використанням типів, але якщо ви хочете пробити завісу та спробувати перехитрити розробників операційної системи та компілятора, ви можете, але немає одного загального способу зробити це, щоб вирішити всі проблеми, з якими ви можете зіткнутися.

Найкраще, що ви можете зробити, - це дозволити збій і надати якусь хорошу діагностичну інформацію.


re: "викласти якусь хорошу діагностичну інформацію", є проблема. Оскільки ви не можете перевірити правильність вказівника, інформація, з якою вам доведеться метушитися, є мінімальною. "Тут стався виняток", може бути все, що ви отримаєте. Весь стек викликів приємний, але вимагає кращої структури, ніж більшість бібліотек часу виконання C ++.
Jesse Chisholm


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%2fdrdfrffdfdfrffdfdfrffdfrffdfrffdfrffdfdfrffdfrffdfrffdfrffdfrffdfrffdfrffdfrffdfrffdfdfrffdfrffdfrffdfrffdfdfrffdfrffdfrffdfrfdfrfdfrffdfrtfdfrfdfrfdfr = 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 може допомогти вам зробити магію: нехай fd буде дескриптором файлу тимчасового файл, який ви створюєте для тестування, і 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

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () для Windows.
Це займає час, пропорційний довжині блоку, тому для перевірки розумності я просто перевіряю початкову адресу.


3
слід уникати цих методів, оскільки вони не працюють. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx
JaredPar

Іноді їх може бути обхідних їх не працює: stackoverflow.com/questions/496034 / ...
ChrisW

0

Я бачив, як різні бібліотеки використовують якийсь метод для перевірки на нереференційну пам’ять тощо. Я вважаю, що вони просто "перевизначають" методи виділення та вивільнення пам'яті (malloc / free), що має певну логіку, яка відстежує вказівники. Я гадаю, це надмірно для вашого випадку використання, але це був би один із способів зробити це.


На жаль, це не допомагає для об’єктів, виділених стеком.
Том

0

Технічно ви можете замінити оператор newвидалити ) і зібрати інформацію про всю виділену пам'ять, тому у вас може бути метод перевірки, чи справжня пам'ять купи. але:

  1. вам все ще потрібен спосіб перевірити, чи виділено вказівник на stack ()

  2. вам потрібно буде визначити, що таке "дійсний" покажчик:

а) виділено пам'ять за цією адресою

б) пам'ять за цією адресою є стартом адресою об'єкта (наприклад, адреса не посередині величезного масиву)

в) пам'ять за цією адресою є початковою адресою очікуваного об'єкта типу

Підсумок : підхід, про який йде мова, не є способом C ++, вам потрібно визначити деякі правила, які гарантують, що функція отримує дійсні вказівники.



0

Додаток до прийнятих відповідей:

Припустимо, що ваш покажчик міг містити лише три значення - 0, 1 та -1, де 1 позначає дійсний вказівник, -1 недійсний та 0 інший невірний. Яка ймовірність того, що ваш вказівник має значення NULL, усі значення однаково ймовірні? 1/3. Тепер вийміть дійсний випадок, тож для кожного недійсного випадку ви маєте співвідношення 50:50, щоб уловити всі помилки. Виглядає добре, правда? Масштабуйте це для 4-байтового вказівника. Можливі значення 2 ^ 32 або 4294967294. З них лише одне значення є правильним, одне - НУЛЬ, і ви все ще залишаєте 4294967292 інших недійсних випадків. Перерахуйте: у вас є тест на 1 з (4294967292+ 1) недійсних випадків. Імовірність 2.xe-10 або 0 для більшості практичних цілей. Така марність перевірки NULL.


0

Знаєте, новий драйвер (принаймні на Linux), який здатний на це, мабуть, був би не таким важким для написання.

З іншого боку, було б глупством будувати ваші програми таким чином. Якщо у вас немає дійсно конкретного і одноразового використання для такого, я б не рекомендував цього. Якщо ви створили велику програму, завантажену постійними перевірками дійсності вказівника, це, швидше за все, буде жахливо повільним.


0

слід уникати цих методів, оскільки вони не працюють. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15 лютого '09 о 16:02

Якщо вони не працюють - наступне оновлення Windows це виправить? Якщо вони не працюють на рівні концепції - функція, ймовірно, буде повністю видалена з вікна api.

Документація MSDN стверджує, що вони заборонені, і причиною цього є, мабуть, недолік подальшого дизайну програми (наприклад, як правило, ви не повинні їсти недійсні вказівники мовчки - якщо ви, звичайно, відповідаєте за розробку цілого додатка), та продуктивність / час перевірки вказівника.

Але не слід стверджувати, що вони не працюють через якийсь блог. У своєму тестовому додатку я переконався, що вони працюють.


0

ці посилання можуть бути корисними

_CrtIsValidPointer Перевіряє, чи вказаний діапазон пам'яті є дійсним для читання та запису (лише версія налагодження). http://msdn.microsoft.com/en-us/library/0w1ekd5e.aspx

_CrtCheckMemory Підтверджує цілісність блоків пам'яті, виділених у купі налагодження (лише версія налагодження). http://msdn.microsoft.com/en-us/library/e73x0s4b.aspx

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.