Mengakses array di luar batas tidak menyebabkan kesalahan, mengapa?

177

Saya menetapkan nilai dalam program C ++ di luar batas seperti ini:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    return 0;
}

Program mencetak 3dan 4. Seharusnya tidak mungkin. Saya menggunakan g ++ 4.3.3

Berikut adalah kompilasi dan jalankan perintah

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4

Hanya ketika menetapkan array[3000]=3000apakah itu memberi saya kesalahan segmentasi.

Jika gcc tidak memeriksa batasan array, bagaimana saya bisa yakin jika program saya benar, karena dapat menyebabkan beberapa masalah serius nanti?

Saya mengganti kode di atas dengan

vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;

dan yang ini juga tidak menghasilkan kesalahan.

seg.server.fault
sumber
3
Pertanyaan terkait: stackoverflow.com/questions/671703/…
TSomKes
16
Kode itu buggy, tentu saja, tetapi menghasilkan perilaku yang tidak terdefinisi . Tidak terdefinisi artinya mungkin berjalan atau tidak berjalan hingga selesai. Tidak ada jaminan kerusakan.
dmckee --- ex-moderator kitten
4
Anda dapat memastikan program Anda benar dengan tidak bermain-main dengan array mentah. Pemrogram C ++ harus menggunakan kelas wadah sebagai gantinya, kecuali dalam pemrograman embedded / OS. Baca ini untuk alasan wadah pengguna. parashift.com/c++-faq-lite/containers.html
jkeys
8
Ingatlah bahwa vektor tidak perlu rentang-periksa menggunakan []. Menggunakan .at () melakukan hal yang sama dengan [] tetapi melakukan pengecekan rentang.
David Thornley
4
A vector tidak secara otomatis mengubah ukuran ketika mengakses elemen di luar batas! Itu hanya UB!
Pavel Minaev

Jawaban:

364

Selamat datang di teman terbaik setiap programmer C / C ++: Perilaku Tidak Terdefinisi .

Ada banyak hal yang tidak ditentukan oleh standar bahasa, karena berbagai alasan. Ini salah satunya.

Secara umum, setiap kali Anda menemukan perilaku yang tidak terdefinisi, apa pun bisa terjadi. Aplikasi mungkin macet, mungkin membeku, mungkin mengeluarkan drive CD-ROM Anda atau membuat setan keluar dari hidung Anda. Ini dapat memformat hard drive Anda atau mengirim email semua porno Anda ke nenek Anda.

Bahkan mungkin, jika Anda benar-benar sial, tampaknya berfungsi dengan benar.

Bahasa hanya mengatakan apa yang harus terjadi jika Anda mengakses elemen dalam batas-batas array. Tidak ditentukan apa yang terjadi jika Anda keluar dari batas. Mungkin tampaknya bekerja hari ini, pada compiler, tetapi tidak legal C atau C ++, dan tidak ada jaminan bahwa hal itu masih akan bekerja pada saat Anda menjalankan program. Atau yang memiliki data penting tidak ditimpa bahkan sekarang, dan Anda hanya tidak mengalami masalah, bahwa itu adalah akan menyebabkan - belum.

Adapun mengapa tidak ada batas memeriksa, ada beberapa aspek untuk jawabannya:

  • Array adalah sisa dari array C. C adalah tentang primitif yang Anda bisa dapatkan. Hanya urutan elemen dengan alamat yang berdekatan. Tidak ada batas memeriksa karena hanya mengekspos memori mentah. Menerapkan mekanisme pemeriksaan batas yang kuat hampir tidak mungkin dilakukan di C.
  • Dalam C ++, pemeriksaan batas dimungkinkan pada tipe kelas. Tetapi sebuah array masih merupakan yang kompatibel dengan C lama. Ini bukan kelas. Selanjutnya, C ++ juga dibangun di atas aturan lain yang membuat pemeriksaan batas menjadi tidak ideal. Prinsip panduan C ++ adalah "Anda tidak membayar apa yang tidak Anda gunakan". Jika kode Anda benar, Anda tidak perlu memeriksa batas, dan Anda tidak harus dipaksa membayar biaya overhead pemeriksaan batas runtime.
  • Jadi C ++ menawarkan std::vectortemplat kelas, yang memungkinkan keduanya. operator[]dirancang agar efisien. Standar bahasa tidak mensyaratkan bahwa ia melakukan pengecekan batas (meskipun tidak melarangnya juga). Vektor juga memiliki at()fungsi anggota yang dijamin untuk melakukan pemeriksaan batas. Jadi di C ++, Anda mendapatkan yang terbaik dari kedua dunia jika Anda menggunakan vektor. Anda mendapatkan kinerja seperti array tanpa memeriksa batas, dan Anda mendapatkan kemampuan untuk menggunakan akses yang diperiksa batas ketika Anda menginginkannya.
jalf
sumber
5
@Jaif: kami telah menggunakan array ini begitu lama, tapi tetap saja mengapa tidak ada tes untuk memeriksa kesalahan sederhana seperti itu?
seg.server.fault
7
Prinsip desain C ++ adalah bahwa itu tidak boleh lebih lambat dari kode C yang setara, dan C tidak melakukan pemeriksaan array terikat. Prinsip desain C pada dasarnya cepat karena ditujukan untuk pemrograman sistem. Array bound checking membutuhkan waktu, dan juga tidak dilakukan. Untuk sebagian besar penggunaan dalam C ++, Anda harus menggunakan wadah daripada array, dan Anda dapat memiliki pilihan cek terikat atau tidak ada cek terikat dengan mengakses elemen melalui masing-masing .at () atau [].
KTC
4
@ jatuhkan cek semacam itu. Jika Anda menulis kode yang benar, Anda tidak ingin membayar harga itu. Karena itu, saya telah menjadi konversi lengkap ke std :: vector's at () method, yang diperiksa IS. Menggunakannya telah menunjukkan beberapa kesalahan dalam apa yang saya pikir kode "benar".
10
Saya percaya versi lama GCC benar-benar meluncurkan Emacs dan sebuah simulasi Towers of Hanoi di dalamnya, ketika bertemu dengan jenis perilaku tertentu yang tidak terdefinisi. Seperti yang saya katakan, apa pun bisa terjadi. ;)
jalf
4
Semuanya sudah dikatakan, jadi ini hanya menjamin tambahan kecil. Debug build bisa sangat memaafkan dalam keadaan ini jika dibandingkan dengan build rilis. Karena informasi debug dimasukkan dalam binari debug, ada sedikit kesempatan bahwa sesuatu yang vital ditimpa. Itulah sebabnya mengapa debug build tampaknya berfungsi dengan baik sementara rilis build crash.
Rich
31

Menggunakan g ++, Anda dapat menambahkan opsi baris perintah: -fstack-protector-all.

Pada contoh Anda ini menghasilkan sebagai berikut:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t

Itu tidak benar-benar membantu Anda menemukan atau memecahkan masalah, tetapi setidaknya segfault akan memberi tahu Anda bahwa ada sesuatu yang salah.

Richard Corden
sumber
10
Saya baru saja menemukan pilihan yang lebih baik: -fmudflap
Hi-Angel
1
@ Hi-Angel: Setara modern adalah -fsanitize=addressyang menangkap bug ini baik pada waktu kompilasi (jika mengoptimalkan) dan saat runtime.
Nate Eldredge
@NateEldredge +1, saat ini saya bahkan menggunakan -fsanitize=undefined,address. Tetapi perlu dicatat bahwa ada kasus sudut langka dengan perpustakaan std, ketika akses di luar batas tidak terdeteksi oleh pembersih . Untuk alasan ini saya akan merekomendasikan untuk menggunakan -D_GLIBCXX_DEBUGopsi tambahan , yang menambahkan lebih banyak cek.
Hai Malaikat
12

g ++ tidak memeriksa batas array, dan Anda mungkin menimpa sesuatu dengan 3,4 tetapi tidak ada yang benar-benar penting, jika Anda mencoba dengan angka yang lebih tinggi Anda akan mendapatkan crash.

Anda hanya menimpa bagian-bagian tumpukan yang tidak digunakan, Anda dapat melanjutkan sampai Anda mencapai akhir ruang yang dialokasikan untuk tumpukan dan akhirnya akan macet

EDIT: Anda tidak memiliki cara untuk menghadapinya, mungkin penganalisa kode statis dapat mengungkapkan kegagalan itu, tetapi itu terlalu sederhana, Anda mungkin memiliki kegagalan yang serupa (tetapi lebih kompleks) tidak terdeteksi bahkan untuk penganalisa statis

Arkaitz Jimenez
sumber
6
Di mana Anda dapatkan jika dari itu di alamat array [3] dan array [4], ada "tidak ada yang benar-benar penting" ??
namezero
7

Itu perilaku yang tidak terdefinisi sejauh yang saya tahu. Jalankan program yang lebih besar dengan itu dan itu akan crash di suatu tempat di sepanjang jalan. Pengecekan batas bukan bagian dari array mentah (atau bahkan std :: vector).

Gunakan std :: vector dengan std::vector::iterator 's sehingga Anda tidak perlu khawatir.

Edit:

Hanya untuk bersenang-senang, jalankan ini dan lihat berapa lama sampai Anda crash:

int main()
{
   int array[1];

   for (int i = 0; i != 100000; i++)
   {
       array[i] = i;
   }

   return 0; //will be lucky to ever reach this
}

Sunting2:

Jangan jalankan itu.

Sunting3:

OK, berikut ini adalah pelajaran singkat tentang array dan hubungannya dengan pointer:

Ketika Anda menggunakan pengindeksan array, Anda benar-benar menggunakan pointer dalam penyamaran (disebut "referensi"), yang secara otomatis direferensikan. Inilah sebabnya mengapa alih-alih * (array [1]), array [1] secara otomatis mengembalikan nilai pada nilai itu.

Saat Anda memiliki pointer ke array, seperti ini:

int array[5];
int *ptr = array;

Kemudian "array" dalam deklarasi kedua benar-benar membusuk menjadi pointer ke array pertama. Ini adalah perilaku yang setara dengan ini:

int *ptr = &array[0];

Ketika Anda mencoba mengakses di luar apa yang Anda alokasikan, Anda benar-benar hanya menggunakan pointer ke memori lain (yang tidak akan dikeluhkan oleh C ++). Mengambil contoh program saya di atas, itu setara dengan ini:

int main()
{
   int array[1];
   int *ptr = array;

   for (int i = 0; i != 100000; i++, ptr++)
   {
       *ptr++ = i;
   }

   return 0; //will be lucky to ever reach this
}

Kompiler tidak akan mengeluh karena dalam pemrograman, Anda sering harus berkomunikasi dengan program lain, terutama sistem operasi. Ini dilakukan dengan pointer cukup sedikit.

jkeys
sumber
3
Saya pikir Anda lupa menambahkan "ptr" pada contoh terakhir Anda di sana. Anda secara tidak sengaja menghasilkan beberapa kode yang terdefinisi dengan baik.
Jeff Lake
1
Haha, lihat mengapa Anda tidak harus menggunakan array mentah?
jkeys
"Inilah sebabnya alih-alih * (array [1]), array [1] secara otomatis mengembalikan nilai pada nilai itu." Apakah Anda yakin * (array [1]) akan berfungsi dengan baik? Saya pikir itu harus * (array +1). ps: Lol, itu seperti mengirim pesan ke masa lalu. Tapi, bagaimanapun:
muyustan
5

Petunjuk

Jika Anda ingin memiliki array ukuran batasan cepat dengan pemeriksaan kesalahan rentang, coba gunakan boost :: array , (juga std :: tr1 :: array dari <tr1/array>itu akan menjadi kontainer standar dalam spesifikasi C ++ berikutnya). Jauh lebih cepat daripada std :: vector. Ini cadangan memori pada heap atau instance kelas, seperti int array [].
Ini adalah contoh kode sederhana:

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> array;
    array.at(0) = 1; // checking index is inside range
    array[1] = 2;    // no error check, as fast as int array[2];
    try
    {
       // index is inside range
       std::cout << "array.at(0) = " << array.at(0) << std::endl;

       // index is outside range, throwing exception
       std::cout << "array.at(2) = " << array.at(2) << std::endl; 

       // never comes here
       std::cout << "array.at(1) = " << array.at(1) << std::endl;  
    }
    catch(const std::out_of_range& r)
    {
        std::cout << "Something goes wrong: " << r.what() << std::endl;
    }
    return 0;
}

Program ini akan mencetak:

array.at(0) = 1
Something goes wrong: array<>: index out of range
Arpegius
sumber
4

C atau C ++ tidak akan memeriksa batas-batas akses array.

Anda mengalokasikan array pada stack. Mengindeks array via array[3]sama dengan * (array + 3), di mana array adalah pointer ke & array [0]. Ini akan menghasilkan perilaku yang tidak terdefinisi.

Salah satu cara untuk menangkap ini kadang-kadang dalam C adalah dengan menggunakan pemeriksa statis, seperti belat . Jika Anda menjalankan:

splint +bounds array.c

di,

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

maka Anda akan mendapatkan peringatan:

array.c: (dalam fungsi utama) array.c: 5: 9: Kemungkinan toko di luar batas: array [1] Tidak dapat menyelesaikan kendala: membutuhkan 0> = 1 yang diperlukan untuk memenuhi prasyarat: membutuhkan maxSet (array @ array .c: 5: 9)> = 1 Tulis memori dapat menulis ke alamat di luar buffer yang dialokasikan.

Karl Voigtland
sumber
Koreksi: sudah dialokasikan oleh OS atau program lain. Dia menimpa memori lain.
jkeys
1
Mengatakan bahwa "C / C ++ tidak akan memeriksa batas" tidak sepenuhnya benar - tidak ada yang menghalangi implementasi yang memenuhi syarat tertentu untuk melakukannya, baik secara default, atau dengan beberapa flag kompilasi. Hanya saja tidak satupun dari mereka yang repot.
Pavel Minaev
3

Anda tentu menimpa tumpukan Anda, tetapi program ini cukup sederhana sehingga efek dari ini tidak diperhatikan.

Paul Dixon
sumber
2
Apakah tumpukan ditimpa atau tidak tergantung pada platform.
Chris Cleeland
3

Jalankan ini melalui Valgrind dan Anda mungkin melihat kesalahan.

Seperti yang ditunjukkan Falaina, valgrind tidak mendeteksi banyak contoh tumpukan korupsi. Saya baru saja mencoba sampel di bawah valgrind, dan memang melaporkan nol kesalahan. Namun, Valgrind dapat berperan dalam menemukan banyak jenis masalah memori lainnya, hanya saja tidak terlalu berguna dalam kasus ini kecuali Anda memodifikasi bulid Anda untuk menyertakan opsi --stack-check. Jika Anda membangun dan menjalankan sampel sebagai

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange

valgrind akan melaporkan kesalahan.

Todd Stout
sumber
2
Sebenarnya, Valgrind sangat buruk dalam menentukan akses array yang salah pada stack. (dan memang seharusnya begitu, yang terbaik yang dapat Anda lakukan adalah menandai seluruh tumpukan sebagai write lokasi yang valid)
Falaina
@ Falaina - poin bagus, tetapi Valgrind dapat mendeteksi setidaknya beberapa kesalahan stack.
Todd Stout
Dan valgrind akan melihat tidak ada yang salah dengan kode karena kompiler cukup pintar untuk mengoptimalkan array dan hanya mengeluarkan literal 3 dan 4. Optimasi itu terjadi sebelum gcc memeriksa batas array yang mengapa peringatan gcc tidak di luar batas melakukan miliki tidak ditampilkan.
Goswin von Brederlow
2

Perilaku yang tidak terdefinisi menguntungkan Anda. Memori apa pun yang Anda hancurkan tampaknya tidak memiliki hal yang penting. Perhatikan bahwa C dan C ++ tidak melakukan batas memeriksa array, jadi hal-hal seperti itu tidak akan tertangkap pada waktu kompilasi atau run.

John Bode
sumber
5
Tidak, Perilaku tidak terdefinisi "bekerja sesuai keinginan Anda" saat crash dengan benar. Ketika tampaknya berhasil, itu tentang skenario terburuk yang mungkin terjadi.
jalf
@ JohnBode: Maka akan lebih baik jika Anda mengoreksi kata-kata sesuai komentar jalf
Destructor
1

Ketika Anda menginisialisasi array dengan int array[2], ruang untuk 2 bilangan bulat dialokasikan; tetapi pengidentifikasi arrayhanya menunjuk ke awal ruang itu. Ketika Anda kemudian mengakses array[3]dan array[4], kompiler kemudian hanya menambah alamat yang menunjuk ke tempat nilai-nilai itu akan, jika array cukup panjang; coba akses sesuatu seperti array[42]tanpa menginisialisasi terlebih dahulu, Anda akan mendapatkan nilai apa pun yang sudah ada di memori di lokasi itu.

Edit:

Info lebih lanjut tentang pointer / array: http://home.netcom.com/~tjensen/ptr/pointers.htm

Nathan Clark
sumber
0

ketika Anda mendeklarasikan array int [2]; Anda memesan 2 ruang memori masing-masing 4 byte (program 32bit). jika Anda mengetik array [4] dalam kode Anda, kode itu masih sesuai dengan panggilan yang valid, tetapi hanya pada saat run time saja ia akan melemparkan pengecualian yang tidak tertangani. C ++ menggunakan manajemen memori manual. Ini sebenarnya adalah celah keamanan yang digunakan untuk meretas program

ini dapat membantu memahami:

int * somepointer;

somepointer [0] = somepointer [5];

yan bellavance
sumber
0

Seperti yang saya mengerti, variabel lokal dialokasikan pada stack, jadi keluar dari batas pada stack Anda sendiri hanya dapat menimpa beberapa variabel lokal lainnya, kecuali Anda terlalu banyak dan melebihi ukuran stack Anda. Karena Anda tidak memiliki variabel lain yang dideklarasikan dalam fungsi Anda - itu tidak menyebabkan efek samping. Cobalah mendeklarasikan variabel / array lain tepat setelah yang pertama dan lihat apa yang akan terjadi dengannya.

Vorber
sumber
0

Ketika Anda menulis 'array [indeks]' di C, ia menerjemahkannya ke instruksi mesin.

Terjemahan berjalan seperti:

  1. 'dapatkan alamat array'
  2. 'dapatkan ukuran dari jenis objek array terdiri dari'
  3. 'kalikan ukuran jenis dengan indeks'
  4. 'tambahkan hasilnya ke alamat array'
  5. 'baca apa yang ada di alamat yang dihasilkan'

Hasilnya membahas sesuatu yang mungkin, atau mungkin tidak, menjadi bagian dari array. Sebagai gantinya kecepatan instruksi mesin yang menyala-nyala Anda kehilangan jaring pengaman komputer yang memeriksa barang-barang untuk Anda. Jika Anda teliti dan berhati-hati, itu bukan masalah. Jika Anda ceroboh atau membuat kesalahan, Anda terbakar. Terkadang mungkin menghasilkan instruksi yang tidak valid yang menyebabkan pengecualian, terkadang tidak.

Jay
sumber
0

Pendekatan yang bagus yang sering saya lihat dan saya telah digunakan sebenarnya adalah untuk menyuntikkan beberapa elemen tipe NULL (atau yang dibuat, seperti uint THIS_IS_INFINITY = 82862863263;) di akhir array.

Kemudian pada pemeriksaan kondisi loop, TYPE *pagesWordsadalah beberapa jenis array pointer:

int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]);

realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1);

pagesWords[pagesWordsLength] = MY_NULL;

for (uint i = 0; i < 1000; i++)
{
  if (pagesWords[i] == MY_NULL)
  {
    break;
  }
}

Solusi ini tidak akan mengatakan apakah array diisi dengan structtipe.

xudre
sumber
0

Seperti yang disebutkan sekarang dalam pertanyaan menggunakan std :: vector :: at akan menyelesaikan masalah dan melakukan pemeriksaan terikat sebelum mengakses.

Jika Anda memerlukan array ukuran konstan yang terletak di tumpukan sebagai kode pertama Anda gunakan C ++ 11 container std :: array baru; sebagai vektor ada std :: array :: at function. Bahkan fungsi tersebut ada di semua kontainer standar yang memiliki arti, yaitu, di mana operator [] didefinisikan :( deque, map, unordered_map) dengan pengecualian std :: bitset di mana ia disebut std :: bitset: :uji.

Mohamed El-Nakib
sumber
0

libstdc ++, yang merupakan bagian dari gcc, memiliki mode debug khusus untuk pengecekan kesalahan. Ini diaktifkan oleh flag compiler -D_GLIBCXX_DEBUG. Di antara hal-hal lain itu tidak terbatas memeriksa std::vectordengan biaya kinerja. Ini demo online dengan versi terbaru gcc.

Jadi sebenarnya Anda dapat melakukan batas memeriksa dengan mode libstdc ++ debug tetapi Anda harus melakukannya hanya saat pengujian karena biayanya kinerja yang terkenal dibandingkan dengan mode libstdc ++ normal.

ks1322
sumber
0

Jika Anda sedikit mengubah program Anda:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    INT NOTHING;
    CHAR FOO[4];
    STRCPY(FOO, "BAR");
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    COUT << FOO << ENDL;
    return 0;
}

(Perubahan modal - masukkan huruf kecil jika Anda akan mencoba ini.)

Anda akan melihat bahwa variabel foo telah dibuang. Kode Anda akan menyimpan nilai ke dalam array [3] dan array [4] yang tidak ada, dan dapat mengambilnya dengan benar, tetapi penyimpanan aktual yang digunakan berasal dari foo .

Jadi Anda dapat "lolos" dengan melampaui batas-batas array dalam contoh asli Anda, tetapi dengan biaya menyebabkan kerusakan di tempat lain - kerusakan yang mungkin terbukti sangat sulit untuk didiagnosis.

Mengapa tidak ada batas otomatis memeriksa - program yang ditulis dengan benar tidak memerlukannya. Setelah itu dilakukan, tidak ada alasan untuk melakukan run-time bounds memeriksa dan melakukan hal itu hanya akan memperlambat program. Yang terbaik untuk mengetahui semuanya selama desain dan pengkodean.

C ++ didasarkan pada C, yang dirancang sedekat mungkin dengan bahasa assembly.

Jennifer
sumber