Menguji petunjuk untuk validitas (C / C ++)


91

Apakah ada cara untuk menentukan (secara programatik, tentu saja) jika sebuah pointer "valid"? Memeriksa NULL itu mudah, tetapi bagaimana dengan 0x00001234? Ketika mencoba untuk membedakan penunjuk jenis ini pengecualian / crash terjadi.

Metode lintas platform lebih disukai, tetapi khusus platform (untuk Windows dan Linux) juga tidak masalah.

Pembaruan untuk klarifikasi: Masalahnya bukan pada petunjuk basi / bebas / tidak diinisialisasi; sebaliknya, saya mengimplementasikan API yang mengambil pointer dari pemanggil (seperti pointer ke string, pegangan file, dll.). Penelepon dapat mengirim (secara sengaja atau tidak sengaja) nilai yang tidak valid sebagai penunjuk. Bagaimana cara mencegah crash?



Saya pikir jawaban positif terbaik untuk linux diberikan oleh George Carrette. Jika itu tidak cukup, pertimbangkan untuk membangun tabel simbol fungsi ke dalam pustaka, atau bahkan tingkat tabel pustaka lain yang tersedia dengan tabel fungsinya sendiri. Kemudian periksa tabel yang tepat tersebut. Tentu saja, jawaban negatif itu juga benar: Anda tidak dapat benar-benar 100% yakin apakah penunjuk fungsi valid atau tidak kecuali Anda memasang banyak batasan tambahan pada aplikasi pengguna.
minghua

Apakah Spesifikasi API benar-benar menentukan kewajiban yang harus dipenuhi oleh implementasi? Ngomong-ngomong, saya berpura-pura tidak berasumsi bahwa Anda adalah pengembang dan desainer. Maksud saya, saya tidak berpikir API akan menentukan sesuatu seperti "Dalam kasus pointer yang tidak valid dilewatkan sebagai argumen, fungsi tersebut harus menangani masalah dan mengembalikan NULL.". API berkewajiban untuk menyediakan layanan dalam kondisi penggunaan yang tepat, bukan dengan peretasan. Namun demikian, tidak ada salahnya menjadi sedikit bodoh. Menggunakan referensi membuat kasus seperti itu tidak menyebar. :)
Poniros

Jawaban:


75

Pembaruan untuk klarifikasi: Masalahnya bukan pada petunjuk yang basi, dibebaskan atau tidak dimulai; sebaliknya, saya mengimplementasikan API yang mengambil pointer dari pemanggil (seperti pointer ke string, pegangan file, dll.). Penelepon dapat mengirim (secara sengaja atau tidak sengaja) nilai yang tidak valid sebagai penunjuk. Bagaimana cara mencegah crash?

Anda tidak dapat melakukan pemeriksaan itu. Tidak ada cara untuk memeriksa apakah sebuah pointer "valid". Anda harus percaya bahwa saat orang menggunakan fungsi yang mengambil penunjuk, orang tersebut tahu apa yang mereka lakukan. Jika mereka melewatkan Anda 0x4211 sebagai nilai penunjuk, maka Anda harus mempercayainya menunjuk ke alamat 0x4211. Dan jika mereka "secara tidak sengaja" mengenai suatu objek, bahkan jika Anda akan menggunakan beberapa fungsi sistem operasi yang menakutkan (IsValidPtr atau apapun), Anda masih akan tergelincir ke dalam bug dan tidak gagal dengan cepat.

Mulailah menggunakan pointer nol untuk memberi isyarat hal semacam ini dan beri tahu pengguna perpustakaan Anda bahwa mereka tidak boleh menggunakan pointer jika mereka cenderung secara tidak sengaja meneruskan pointer yang tidak valid, serius :)


Ini mungkin jawaban yang benar tetapi saya pikir fungsi sederhana yang memeriksa lokasi memori hexspeak umum akan berguna untuk debugging umum ... Saat ini saya memiliki penunjuk yang terkadang menunjuk ke 0xfeeefeee dan jika saya memiliki fungsi sederhana yang saya bisa gunakan untuk merica menegaskan sekitar Ini akan membuatnya lebih mudah untuk menemukan pelakunya ... EDIT: Meskipun tidak akan sulit untuk menulisnya sendiri, kurasa ..
Quant

@quant masalahnya adalah bahwa beberapa kode C dan C ++ dapat melakukan aritmatika penunjuk pada alamat yang tidak valid tanpa memeriksa (berdasarkan prinsip sampah-masuk, sampah-keluar) dan dengan demikian akan meneruskan penunjuk "yang dimodifikasi secara aritmatika" dari salah satu sumur tersebut alamat tidak valid yang diketahui. Kasus umum mencari metode dari vtable yang tidak ada berdasarkan alamat objek yang tidak valid atau salah satu jenis yang salah, atau hanya membaca bidang dari pointer ke struct yang tidak mengarah ke satu.
rwong

Ini pada dasarnya berarti bahwa Anda hanya dapat mengambil indeks array dari dunia luar. API yang harus mempertahankan diri dari pemanggil tidak boleh memiliki pointer di antarmuka. Namun, akan tetap bagus untuk memiliki makro untuk digunakan dalam pernyataan tentang validitas pointer (yang harus Anda miliki secara internal). Jika sebuah pointer dijamin untuk menunjuk ke dalam sebuah array yang titik awal dan panjangnya diketahui, itu bisa diperiksa secara eksplisit. Lebih baik mati karena pelanggaran pernyataan (kesalahan terdokumentasi) daripada deref (kesalahan tidak berdokumen).
Rob

34

Berikut adalah tiga cara mudah untuk program C di Linux untuk mendapatkan introspektif tentang status memori tempat program tersebut berjalan, dan mengapa pertanyaan tersebut memiliki jawaban canggih yang sesuai dalam beberapa konteks.

  1. Setelah memanggil getpagesize () dan membulatkan penunjuk ke batas halaman, Anda bisa memanggil mincore () untuk mengetahui apakah halaman itu valid dan jika itu merupakan bagian dari proses kerja set. Perhatikan bahwa ini membutuhkan beberapa sumber kernel, jadi Anda harus melakukan tolok ukur dan menentukan apakah memanggil fungsi ini benar-benar sesuai di api Anda. Jika api Anda akan menangani interupsi, atau membaca dari port serial ke dalam memori, sebaiknya panggil ini untuk menghindari perilaku yang tidak dapat diprediksi.
  2. Setelah memanggil stat () untuk menentukan apakah ada direktori / proc / self yang tersedia, Anda dapat membuka dan membaca / proc / self / maps untuk menemukan informasi tentang wilayah tempat penunjuk berada. Pelajari halaman manual untuk proc, proses informasi sistem file semu. Jelas ini relatif mahal, tetapi Anda mungkin bisa lolos dengan menyimpan hasil parse ke dalam larik yang dapat Anda cari secara efisien menggunakan pencarian biner. Pertimbangkan juga / proc / self / smaps. Jika api Anda adalah untuk komputasi kinerja tinggi maka program akan ingin mengetahui tentang / proc / self / numa yang didokumentasikan di bawah halaman manual untuk numa, arsitektur memori yang tidak seragam.
  3. Panggilan get_mempolicy (MPOL_F_ADDR) sesuai untuk pekerjaan api komputasi berkinerja tinggi di mana terdapat beberapa utas eksekusi dan Anda mengelola pekerjaan Anda agar memiliki afinitas untuk memori yang tidak seragam karena hal itu berkaitan dengan inti cpu dan sumber daya soket. Api seperti itu tentu saja juga akan memberi tahu Anda jika sebuah pointer valid.

Di bawah Microsoft Windows ada fungsi QueryWorkingSetEx yang didokumentasikan di bawah API Status Proses (juga di API NUMA). Sebagai akibat wajar dari pemrograman NUMA API yang canggih, fungsi ini juga akan memungkinkan Anda melakukan pekerjaan sederhana "pointer pengujian untuk validitas (C / C ++)", karena itu sepertinya tidak akan digunakan lagi setidaknya selama 15 tahun.


13
Jawaban pertama yang tidak mencoba bermoral tentang pertanyaan itu sendiri dan benar-benar menjawabnya dengan sempurna. Orang terkadang tidak menyadari bahwa seseorang benar-benar membutuhkan pendekatan debugging semacam ini untuk menemukan bug di misalnya perpustakaan pihak ke-3 atau dalam kode lama karena bahkan valgrind hanya menemukan petunjuk liar ketika benar-benar mengaksesnya, bukan misalnya jika Anda ingin memeriksa validitas pointer secara teratur dalam tabel cache yang telah ditimpa dari beberapa tempat lain dalam kode Anda ...
lumpidu

Ini harus menjadi jawaban yang diterima. Saya melakukannya dengan cara yang sama pada platform non-linux. Pada dasarnya ini adalah mengekspos informasi proses ke proses itu sendiri. Dengan aspek ini, tampaknya windows melakukan pekerjaan yang lebih baik daripada linux dengan menampilkan informasi yang lebih bermakna melalui API status proses.
minghua

31

Mencegah crash yang disebabkan oleh pemanggil yang mengirimkan pointer yang tidak valid adalah cara yang baik untuk membuat bug diam yang sulit ditemukan.

Bukankah lebih baik bagi programmer yang menggunakan API Anda untuk mendapatkan pesan yang jelas bahwa kodenya palsu dengan cara menabraknya daripada menyembunyikannya?


8
Namun dalam beberapa kasus, memeriksa penunjuk yang buruk segera saat API dipanggil adalah cara Anda gagal lebih awal. Misalnya, bagaimana jika API menyimpan penunjuk dalam struktur data yang hanya akan ditangguhkan nanti? Kemudian melewati API, pointer yang buruk akan menyebabkan crash di beberapa titik kemudian secara acak. Dalam hal ini, akan lebih baik jika gagal lebih cepat, pada panggilan API tempat nilai buruk pertama kali diperkenalkan.
peterflynn

28

Di Win32 / 64 ada cara untuk melakukan ini. Mencoba membaca pointer dan menangkap hasil eksekusi SEH yang akan dilemparkan jika gagal. Jika tidak melempar, maka itu penunjuk yang valid.

Masalah dengan metode ini adalah bahwa ia hanya mengembalikan apakah Anda dapat membaca data dari penunjuk atau tidak. Tidak ada jaminan tentang keamanan tipe atau sejumlah invarian lainnya. Secara umum metode ini baik untuk hal lain selain mengatakan "ya, saya dapat membaca tempat tertentu dalam ingatan pada waktu yang sekarang telah berlalu".

Singkatnya, Jangan lakukan ini;)

Raymond Chen memiliki entri blog tentang subjek ini: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@ Tim, tidak ada cara untuk melakukan itu di C ++.
JaredPar

6
Ini hanya "jawaban yang benar" jika Anda mendefinisikan "penunjuk yang valid" sebagai "tidak menyebabkan pelanggaran akses / segfault". Saya lebih suka mendefinisikannya sebagai "menunjuk ke data bermakna yang dialokasikan untuk tujuan Anda akan menggunakannya". Saya berpendapat itu adalah definisi yang lebih baik dari validitas pointer ...;)
jalf

Bahkan jika penunjuk valid tidak dapat diperiksa dengan cara ini. Pikirkan thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {tidur (1); hapus p; ...}
Christopher

2
@ Christopher, sangat benar. Saya seharusnya mengatakan "Saya dapat membaca tempat tertentu dalam ingatan pada waktu yang sekarang telah berlalu"
JaredPar

@JaredPar: Saran yang sangat buruk. Dapat memicu halaman pelindung, sehingga tumpukan tidak akan diperluas nanti atau sesuatu yang sama bagusnya.
Deduplicator

16

AFAIK tidak mungkin. Anda harus mencoba menghindari situasi ini dengan selalu mengatur pointer ke NULL setelah mengosongkan memori.


4
Menetapkan pointer ke null tidak memberi Anda apa-apa, kecuali mungkin rasa aman yang salah.

Itu tidak benar. Khususnya di C ++ Anda dapat menentukan apakah akan menghapus objek anggota dengan memeriksa null. Juga perhatikan bahwa, dalam C ++, menghapus null-pointer adalah valid, oleh karena itu menghapus objek tanpa syarat di destruktor sangat populer.
Ferdinand Beyer

4
int * p = new int (0); int * p2 = p; hapus p; p = NULL; hapus p2; // crash

1
zabzonk, dan ?? apa yang dia katakan adalah bahwa Anda dapat menghapus pointer nol. p2 bukan penunjuk null, tetapi penunjuk tidak valid. Anda harus mengaturnya ke nol sebelumnya.
Johannes Schaub - litb

2
Jika Anda memiliki alias ke memori yang ditunjuk, hanya satu dari mereka yang akan disetel ke NULL, alias lain tergantung di sekitarnya.
jdehaan


7

Mengenai jawabannya sedikit di utas ini:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () untuk Windows.

Saran saya adalah menjauh dari mereka, seseorang telah memposting yang ini: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Posting lain tentang topik yang sama dan oleh penulis yang sama (menurut saya) adalah yang ini: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr seharusnya benar-benar disebut CrashProgramRandomly ").

Jika pengguna API Anda mengirimkan data yang buruk, biarkan itu mogok. Jika masalahnya adalah bahwa data yang diteruskan tidak digunakan sampai nanti (dan itu membuat lebih sulit untuk menemukan penyebabnya), tambahkan mode debug di mana string, dll. Dicatat saat masuk. Jika mereka buruk maka akan terlihat jelas (dan mungkin crash). Jika ini sering terjadi, mungkin ada baiknya Anda mengeluarkan API dari proses dan membiarkannya merusak proses API alih-alih proses utama.


Mungkin cara lain adalah dengan menggunakan _CrtIsValidHeapPointer . Fungsi ini akan mengembalikan TRUE jika penunjuk valid, dan memunculkan pengecualian saat penunjuk dibebaskan. Seperti yang didokumentasikan, fungsi ini hanya tersedia di debug CRT.
Crend King

6

Pertama, saya tidak melihat ada gunanya mencoba melindungi diri Anda dari penelepon yang dengan sengaja mencoba menyebabkan kecelakaan. Mereka dapat dengan mudah melakukan ini dengan mencoba mengakses sendiri melalui penunjuk yang tidak valid. Ada banyak cara lain - cara ini dapat menimpa memori atau tumpukan Anda. Jika Anda perlu melindungi dari hal semacam ini maka Anda perlu menjalankan dalam proses terpisah menggunakan soket atau IPC lain untuk komunikasi.

Kami menulis cukup banyak perangkat lunak yang memungkinkan mitra / pelanggan / pengguna untuk memperluas fungsionalitas. Tidak dapat dipungkiri bahwa setiap bug dilaporkan kepada kami terlebih dahulu sehingga berguna untuk dapat dengan mudah menunjukkan bahwa masalahnya ada pada kode plugin. Selain itu, ada masalah keamanan dan beberapa pengguna lebih dipercaya daripada yang lain.

Kami menggunakan sejumlah metode berbeda tergantung pada kinerja / persyaratan throughput dan kepercayaan. Dari yang paling disukai:

  • proses terpisah menggunakan soket (sering melewatkan data sebagai teks).

  • proses terpisah menggunakan memori bersama (jika sejumlah besar data lewat).

  • proses yang sama memisahkan utas melalui antrian pesan (jika sering pesan singkat).

  • proses yang sama, utas terpisah, semua data yang lewat dialokasikan dari kumpulan memori.

  • proses yang sama melalui panggilan prosedur langsung - semua data yang lewat dialokasikan dari kolam memori.

Kami mencoba untuk tidak pernah menggunakan apa yang Anda coba lakukan saat berurusan dengan perangkat lunak pihak ketiga - terutama ketika kami diberi plug-in / library sebagai biner daripada kode sumber.

Penggunaan kumpulan memori cukup mudah dalam banyak situasi dan tidak perlu tidak efisien. Jika ANDA mengalokasikan data di tempat pertama maka itu mudah untuk memeriksa pointer terhadap nilai yang Anda alokasikan. Anda juga dapat menyimpan durasi yang dialokasikan dan menambahkan nilai "ajaib" sebelum dan sesudah data untuk memeriksa jenis data yang valid dan pembengkakan data.


5

Di Unix Anda harus dapat menggunakan kernel syscall yang melakukan pemeriksaan pointer dan mengembalikan EFAULT, seperti:

#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;
}

kembali:

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

Mungkin ada syscall yang lebih baik untuk digunakan daripada open () [mungkin akses], karena ada kemungkinan hal ini dapat mengarah ke jalur kode pembuatan file aktual, dan persyaratan penutupan berikutnya.


1
Ini adalah peretasan yang brilian. Saya ingin sekali melihat saran tentang syscall yang berbeda untuk memvalidasi rentang memori, terutama jika mereka dapat dijamin tidak memiliki efek samping. Anda dapat membuat deskriptor file tetap terbuka untuk menulis ke / dev / null untuk menguji apakah buffer ada dalam memori yang dapat dibaca, tetapi mungkin ada solusi yang lebih sederhana. Yang terbaik yang dapat saya temukan adalah symlink (ptr, "") yang akan menyetel errno ke 14 pada alamat yang buruk atau 2 pada alamat yang baik, tetapi perubahan kernel dapat menukar urutan verifikasi.
Preston

1
@ Preston Di DB2 saya pikir kami dulu menggunakan akses unistd.h's (). Saya menggunakan open () di atas karena sedikit kurang jelas, tetapi Anda mungkin benar bahwa ada banyak syscall yang mungkin untuk digunakan. Windows dulunya memiliki API pemeriksaan penunjuk eksplisit, tetapi ternyata tidak aman untuk utas (saya pikir itu menggunakan SEH untuk mencoba menulis dan kemudian memulihkan batas-batas rentang memori.)
Peeter Joot

4

Saya mendapat banyak simpati dengan pertanyaan Anda, karena saya sendiri dalam posisi yang hampir sama. Saya menghargai apa yang dikatakan banyak balasan, dan mereka benar - rutinitas yang memasok penunjuk harus menyediakan penunjuk yang valid. Dalam kasus saya, hampir tidak dapat dibayangkan bahwa mereka dapat merusak penunjuk - tetapi jika memang demikian berhasil, itu akan menjadi MY perangkat lunak yang crash, dan ME yang akan disalahkan :-(

Persyaratan saya bukanlah saya melanjutkan setelah kesalahan segmentasi - itu akan berbahaya - saya hanya ingin melaporkan apa yang terjadi pada pelanggan sebelum menghentikan sehingga mereka dapat memperbaiki kode mereka daripada menyalahkan saya!

Beginilah cara saya melakukannya (di Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Untuk memberikan sinopsis:

#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);
}

Sekarang ini tampaknya berperilaku seperti yang saya harapkan (ini mencetak pesan kesalahan, lalu menghentikan program) - tetapi jika seseorang dapat menemukan kesalahan, beri tahu saya!


Jangan gunakan exit(), ini mengelak dari RAII dan dengan demikian dapat menyebabkan kebocoran sumber daya.
Sebastian Mach

Menarik - apakah ada cara lain untuk mengakhiri dengan rapi dalam situasi ini? Dan apakah pernyataan keluar satu-satunya masalah dengan melakukannya seperti ini? Saya perhatikan saya mendapatkan "-1" - apakah itu hanya karena 'keluar'?
Mike Sadler

Ups, saya menyadari ini untuk situasi yang sangat luar biasa. Saya baru saja melihat exit()dan Bel Alarm C ++ Portabel saya mulai berdering. Seharusnya tidak masalah dalam situasi khusus Linux ini, di mana program Anda akan keluar, maaf untuk kebisingannya.
Sebastian Mach

1
sinyal (2) tidak portabel. Gunakan sigaction (2). man 2 signaldi Linux memiliki paragraf yang menjelaskan alasannya.
rptb1

1
Dalam situasi ini saya biasanya memanggil abort (3) daripada exit (3) karena lebih mungkin menghasilkan semacam debugging backtrace yang dapat Anda gunakan untuk mendiagnosis masalah post-mortem. Di sebagian besar Unixen, abort (3) akan membuang inti (jika dump inti diizinkan) dan di Windows akan menawarkan untuk meluncurkan debugger jika diinstal.
rptb1

2

Tidak ada ketentuan dalam C ++ untuk menguji validitas pointer sebagai kasus umum. Seseorang dapat dengan jelas berasumsi bahwa NULL (0x00000000) itu buruk, dan berbagai kompiler dan pustaka suka menggunakan "nilai khusus" di sana-sini untuk membuat debugging lebih mudah (Misalnya, jika saya pernah melihat penunjuk muncul sebagai 0xCECECECE di studio visual, saya tahu Saya melakukan sesuatu yang salah) tetapi kenyataannya adalah karena pointer hanyalah indeks ke dalam memori, hampir tidak mungkin untuk mengetahui hanya dengan melihat pointer apakah itu indeks yang "benar".

Ada berbagai trik yang dapat Anda lakukan dengan dynamic_cast dan RTTI seperti untuk memastikan bahwa objek yang ditunjuk adalah jenis yang Anda inginkan, tetapi semuanya mengharuskan Anda menunjuk ke sesuatu yang valid di tempat pertama.

Jika Anda ingin memastikan bahwa program Anda dapat mendeteksi pointer yang "tidak valid" maka saran saya adalah: Setel setiap pointer yang Anda deklarasikan ke NULL atau alamat yang valid segera setelah pembuatan dan setel ke NULL segera setelah membebaskan memori yang ditunjuknya. Jika Anda rajin melakukan praktik ini, maka memeriksa NULL adalah semua yang Anda butuhkan.


Konstanta penunjuk nol dalam C ++ (atau C, dalam hal ini), diwakili oleh nol integral konstan. Banyak implementasi menggunakan semua-biner-nol untuk mewakilinya, tapi itu bukan sesuatu yang bisa diandalkan.
David Thornley

2

Tidak ada cara portabel untuk melakukan ini, dan melakukannya untuk platform tertentu bisa jadi sulit dan tidak mungkin. Bagaimanapun, Anda tidak boleh menulis kode yang bergantung pada pemeriksaan semacam itu - jangan biarkan penunjuk mengambil nilai yang tidak valid sejak awal.


2

Mengatur pointer ke NULL sebelum dan sesudah menggunakan adalah teknik yang baik. Ini mudah dilakukan di C ++ jika Anda mengelola pointer dalam kelas misalnya (string):

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);
}

Pertanyaan yang diperbarui membuat jawaban saya salah, tentu saja (atau setidaknya jawaban untuk pertanyaan lain). Saya setuju dengan jawaban yang pada dasarnya mengatakan, biarkan mereka crash jika mereka menyalahgunakan api. Anda tidak dapat menghentikan orang-orang yang memukul ibu jari mereka sendiri dengan palu ...
Tim Ring

2

Bukan kebijakan yang sangat baik untuk menerima pointer arbitrer sebagai parameter input dalam API publik. Lebih baik memiliki tipe "data biasa" seperti integer, string atau struct (maksud saya struct klasik dengan data biasa di dalamnya, tentu saja; secara resmi apa pun bisa menjadi struct).

Mengapa? Nah karena seperti yang dikatakan orang lain tidak ada cara standar untuk mengetahui apakah Anda telah diberi penunjuk yang valid atau yang menunjuk ke sampah.

Namun terkadang Anda tidak punya pilihan - API Anda harus menerima pointer.

Dalam kasus ini, adalah tugas pemanggil untuk memberikan pointer yang baik. NULL dapat diterima sebagai nilai, tetapi bukan penunjuk ke junk.

Bisakah Anda memeriksa ulang dengan cara apa pun? Nah, apa yang saya lakukan dalam kasus seperti itu adalah mendefinisikan invarian untuk jenis yang ditunjuk pointer, dan memanggilnya ketika Anda mendapatkannya (dalam mode debug). Setidaknya jika invarian gagal (atau crash) Anda tahu bahwa Anda diberikan nilai yang buruk.

// 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: "meneruskan tipe data biasa ... seperti string" Tetapi dalam C ++ string paling sering diteruskan sebagai penunjuk ke karakter, (char *) atau (const char *), jadi Anda kembali ke penunjuk yang lewat. Dan contoh Anda mengirimkan in_person sebagai referensi, bukan sebagai penunjuk, jadi perbandingan (in_person! = NULL) menyiratkan ada beberapa perbandingan objek / penunjuk yang ditentukan di kelas Person.
Jesse Chisholm

@ JesseChisholm Dengan string yang saya maksud adalah string, yaitu std :: string. Saya sama sekali tidak merekomendasikan penggunaan char * sebagai cara untuk menyimpan string atau menyebarkannya. Jangan lakukan itu.
Daniel Daranas

@JesseChisholm Untuk beberapa alasan, saya membuat kesalahan saat menjawab pertanyaan ini lima tahun lalu. Jelas, tidak masuk akal untuk memeriksa apakah Person & is NULL. Itu bahkan tidak bisa dikompilasi. Saya bermaksud menggunakan petunjuk, bukan referensi. Saya memperbaikinya sekarang.
Daniel Daranas

1

Seperti yang dikatakan orang lain, Anda tidak dapat dengan andal mendeteksi pointer yang tidak valid. Pertimbangkan beberapa bentuk yang mungkin diambil penunjuk yang tidak valid:

Anda bisa memiliki penunjuk nol. Itu salah satu yang dapat dengan mudah Anda periksa dan lakukan sesuatu.

Anda dapat memiliki penunjuk ke suatu tempat di luar memori yang valid. Apa yang merupakan memori valid bervariasi tergantung pada bagaimana lingkungan waktu proses sistem Anda mengatur ruang alamat. Pada sistem Unix, ini biasanya berupa ruang alamat virtual yang dimulai dari 0 dan menuju sejumlah besar megabyte. Pada sistem tertanam, ukurannya bisa sangat kecil. Ini mungkin tidak dimulai dari 0, dalam hal apapun. Jika aplikasi Anda berjalan dalam mode supervisor atau yang setara, penunjuk Anda mungkin merujuk ke alamat asli, yang mungkin dicadangkan dengan memori asli atau tidak.

Anda dapat memiliki penunjuk ke suatu tempat di dalam memori valid Anda, bahkan di dalam segmen data, bss, stack, atau heap, tetapi tidak mengarah ke objek yang valid. Variannya adalah pointer yang digunakan untuk menunjuk ke objek yang valid, sebelum sesuatu yang buruk terjadi pada objek tersebut. Hal-hal buruk dalam konteks ini termasuk deallocation, kerusakan memori, atau kerusakan pointer.

Anda bisa saja memiliki penunjuk ilegal yang jelas, seperti penunjuk dengan penyelarasan ilegal untuk hal yang dirujuk.

Masalah menjadi lebih buruk ketika Anda mempertimbangkan arsitektur berbasis segmen / offset dan implementasi pointer aneh lainnya. Hal semacam ini biasanya disembunyikan dari pengembang oleh kompiler yang baik dan penggunaan tipe yang bijaksana, tetapi jika Anda ingin menembus tabir dan mencoba mengakali sistem operasi dan pengembang kompiler, yah, Anda bisa, tetapi tidak ada satu cara umum untuk melakukannya yang akan menangani semua masalah yang mungkin Anda hadapi.

Hal terbaik yang dapat Anda lakukan adalah membiarkan crash dan mengeluarkan beberapa informasi diagnostik yang baik.


re: "keluarkan beberapa informasi diagnostik yang baik", ada intinya. Karena Anda tidak dapat memeriksa validitas penunjuk, informasi yang perlu Anda keributkan menjadi minimal. "Pengecualian terjadi di sini," mungkin hanya itu yang Anda dapatkan. Seluruh tumpukan panggilan bagus, tetapi membutuhkan kerangka kerja yang lebih baik daripada kebanyakan perpustakaan waktu berjalan C ++ menyediakan.
Jesse Chisholm


1

Secara umum, hal itu tidak mungkin dilakukan. Inilah satu kasus yang sangat buruk:

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);

Pada banyak platform, ini akan dicetak:

[0 1 2]

Anda memaksa sistem runtime untuk salah menafsirkan bit memori, tetapi dalam kasus ini tidak akan mogok, karena semua bit masuk akal. Ini adalah bagian dari desain bahasa (lihat C-gaya polimorfisme dengan struct inaddr, inaddr_in, inaddr_in6), sehingga Anda tidak dapat dipercaya melindungi terhadap itu pada platform apapun.


1

Sulit dipercaya betapa banyak informasi menyesatkan yang dapat Anda baca di artikel di atas ...

Dan bahkan dalam dokumentasi microsoft msdn IsBadPtr diklaim dilarang. Baiklah - Saya lebih suka aplikasi yang berfungsi daripada mogok. Bahkan jika jangka waktu bekerja mungkin tidak berfungsi dengan benar (selama pengguna akhir dapat melanjutkan aplikasi).

Dengan googling, saya belum menemukan contoh berguna untuk windows - menemukan solusi untuk aplikasi 32-bit,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2FsRc%2Ftcidp2 = 2

tetapi saya juga perlu mendukung aplikasi 64-bit, jadi solusi ini tidak berhasil untuk saya.

Tapi saya telah memanen kode sumber anggur, dan berhasil memasak jenis kode serupa yang akan berfungsi untuk aplikasi 64-bit juga - dengan melampirkan kode di sini:

#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;  
}  

Dan kode berikut dapat mendeteksi apakah pointer valid atau tidak, Anda mungkin perlu menambahkan beberapa pemeriksaan 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();

Ini mendapat nama kelas dari pointer - Saya pikir itu harus cukup untuk kebutuhan Anda.

Satu hal yang saya masih khawatirkan adalah kinerja pemeriksaan penunjuk - di snipet kode di atas sudah ada 3-4 panggilan API yang dibuat - mungkin berlebihan untuk aplikasi kritis waktu.

Akan lebih baik jika seseorang dapat mengukur overhead pemeriksaan pointer dibandingkan, misalnya dengan panggilan C # / managed c ++.


1

Memang, sesuatu dapat dilakukan dalam keadaan tertentu: misalnya jika Anda ingin memeriksa apakah string penunjuk string valid, menggunakan write (fd, buf, szie) syscall dapat membantu Anda melakukan keajaiban: biarkan fd menjadi deskriptor file sementara file yang Anda buat untuk pengujian, dan buf yang menunjuk ke string yang Anda uji, jika pointer tidak valid write () akan mengembalikan -1 dan errno disetel ke EFAULT yang menunjukkan bahwa buf berada di luar ruang alamat Anda yang dapat diakses.


1

Berikut ini berfungsi di Windows (seseorang menyarankannya sebelumnya):

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

Fungsi tersebut harus metode statis, mandiri atau statis dari beberapa kelas. Untuk menguji di read-only, salin data di buffer lokal. Untuk menguji menulis tanpa mengubah isinya, tulislah kembali. Anda hanya dapat menguji alamat pertama / terakhir. Jika pointer tidak valid, kontrol akan diteruskan ke 'doSomething', dan kemudian di luar tanda kurung. Hanya saja, jangan gunakan apa pun yang membutuhkan destruktor, seperti CString.


1

Di Windows saya menggunakan kode ini:

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) + "!");
}

Pemakaian:

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


0

Saya telah melihat berbagai perpustakaan menggunakan beberapa metode untuk memeriksa memori yang tidak direferensikan dan semacamnya. Saya percaya mereka hanya "menimpa" alokasi memori dan metode deallocation (malloc / free), yang memiliki beberapa logika yang melacak petunjuknya. Saya kira ini berlebihan untuk kasus penggunaan Anda, tetapi ini akan menjadi salah satu cara untuk melakukannya.


Sayangnya, itu tidak membantu untuk objek dengan alokasi stack.
Tom

0

Secara teknis, Anda dapat mengganti operator new (dan menghapus ) dan mengumpulkan informasi tentang semua memori yang dialokasikan, sehingga Anda dapat memiliki metode untuk memeriksa apakah memori heap valid. tapi:

  1. Anda masih memerlukan cara untuk memeriksa apakah pointer dialokasikan di stack ()

  2. Anda perlu menentukan apa itu penunjuk 'valid':

a) memori pada alamat itu dialokasikan

b) memori di alamat itu dimulai objek (misalnya alamat bukan di tengah array besar)

c) memori pada alamat itu adalah alamat awal dari objek yang diharapkan jenis yang

Intinya : pendekatan yang dimaksud bukanlah cara C ++, Anda perlu mendefinisikan beberapa aturan yang memastikan bahwa fungsi menerima petunjuk yang valid.



0

Tambahan pada jawaban yang diterima:

Asumsikan bahwa penunjuk Anda hanya dapat menampung tiga nilai - 0, 1 dan -1 di mana 1 menandakan penunjuk yang valid, -1 yang tidak valid dan 0 yang lain tidak valid. Berapa probabilitas pointer Anda adalah NULL, semua nilai kemungkinannya sama? 1/3. Sekarang, keluarkan kasus yang valid, jadi untuk setiap kasus yang tidak valid, Anda memiliki rasio 50:50 untuk menangkap semua kesalahan. Terlihat bagus kan? Skala ini untuk penunjuk 4-byte. Ada 2 ^ 32 atau 4294967294 kemungkinan nilai. Dari jumlah tersebut, hanya SATU nilai yang benar, satu adalah NULL, dan Anda masih memiliki 4294967292 kasus tidak valid lainnya. Hitung ulang: Anda memiliki tes untuk 1 dari (4294967292+ 1) kasus tidak valid. Probabilitas 2.xe-10 atau 0 untuk sebagian besar tujuan praktis. Begitulah kesia-siaan pemeriksaan NULL.


0

Anda tahu, driver baru (setidaknya di Linux) yang mampu melakukan ini mungkin tidak akan sulit untuk ditulis.

Di sisi lain, adalah bodoh untuk membangun program Anda seperti ini. Kecuali Anda memiliki beberapa penggunaan yang benar-benar spesifik dan tunggal untuk hal seperti itu, saya tidak akan merekomendasikannya. Jika Anda membangun aplikasi besar yang dimuat dengan pemeriksaan validitas penunjuk konstan, kemungkinan akan sangat lambat.


0

Anda harus menghindari metode ini karena tidak berhasil. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15 Feb '09 pukul 16:02

Jika tidak berfungsi - pembaruan windows berikutnya akan memperbaikinya? Jika mereka tidak bekerja pada level konsep - fungsi mungkin akan dihapus dari api windows sepenuhnya.

Dokumentasi MSDN mengklaim bahwa mereka dilarang, dan alasan untuk ini mungkin adalah cacat dari desain aplikasi lebih lanjut (misalnya umumnya Anda tidak boleh makan petunjuk yang tidak valid secara diam-diam - jika Anda bertanggung jawab atas desain seluruh aplikasi, tentu saja), dan kinerja / waktu pemeriksaan penunjuk.

Tetapi Anda tidak boleh mengklaim bahwa mereka tidak berfungsi karena beberapa blog. Dalam aplikasi pengujian saya, saya telah memverifikasi bahwa mereka berfungsi.


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.