Bir diziye sınırların dışında erişmek hata vermez, neden?


177

Bu gibi sınırların dışında bir C ++ programında değerler atamak:

#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 3ve yazdırır 4. Bu mümkün olmamalı. G ++ 4.3.3 kullanıyorum

İşte derleme ve çalıştırma komutu

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

Sadece atama yaparken array[3000]=3000bana bir segmentasyon hatası veriyor.

Gcc dizi sınırlarını kontrol etmezse, daha sonra bazı ciddi sorunlara yol açabileceğinden programımın doğru olup olmadığından nasıl emin olabilirim?

Yukarıdaki kodu ile değiştirdim

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

ve bu da hata üretmez.



16
Kod elbette buggy, ancak tanımlanmamış davranış üretir . Tanımsız, tamamlanabileceği veya tamamlanmayabileceği anlamına gelir. Çökme garantisi yoktur.
dmckee --- eski moderatör kedi yavrusu

4
Ham dizilerle uğraşmadan programınızın doğru olduğundan emin olabilirsiniz. C ++ programcıları, gömülü / işletim sistemi programlaması hariç, kapsayıcı sınıflarını kullanmalıdır. Kullanıcı kapsayıcıları için nedenleri okuyun. parashift.com/c++-faq-lite/containers.html
jkeys

8
Vektörlerin mutlaka [] kullanarak aralık kontrolü yapmadığını unutmayın. .At () kullanmak [] ile aynı şeyi yapar, ancak aralık kontrolü yapar.
David Thornley

4
A vector , sınır ötesi öğelere erişirken otomatik olarak yeniden boyutlandırılmaz! Sadece UB!
Pavel Minaev

Yanıtlar:


364

Her C / C ++ programcısının en iyi arkadaşına hoş geldiniz: Tanımsız Davranış .

Çeşitli nedenlerle dil standardı tarafından belirtilmeyen bir çok şey var. Bu onlardan biri.

Genelde, tanımlanmamış davranışı karşılaştığınızda, içinde bir şey olabilir. Uygulama çökebilir, donabilir, CD-ROM sürücünüzü çıkarabilir veya şeytanların burnunuzdan çıkmasını sağlayabilir. Sabit sürücünüzü biçimlendirebilir veya tüm pornolarınızı büyükannenize e-posta ile gönderebilir.

Gerçekten şanssız olsanız bile, düzgün çalışıyor gibi görünebilir .

Dil, öğelere erişirseniz ne olması gerektiğini söyler , bir dizinin sınırları içindeki . Sınırların dışına çıkarsanız ne olduğu tanımsız bırakılır. Bu olabilir görünmek sizin derleyicilerde bugün işe, ama yasal C veya C ++ değildir ve hala programı çalıştırmak dahaki sefere çalışacağız dair bir garanti yoktur. Ya da şimdi bile değil üzerine yazılır temel veri var ve sadece o, bir sorun ile karşılaşmadım olması edilir neden oluyor - henüz.

Neden sınır kontrolü olmadığına gelince , cevabın birkaç yönü vardır:

  • Bir dizi C'den artık bir şeydir. C dizileri alabileceğiniz kadar ilkeldir. Sadece bitişik adresleri olan bir dizi eleman. Ham belleği açığa çıkardığı için sınır kontrolü yoktur. C'de sağlam bir sınır kontrol mekanizması uygulamak neredeyse imkansız olurdu.
  • C ++ 'da, sınıf türlerinde sınır denetimi mümkündür. Ancak bir dizi hala düz eski C uyumlu dizidir. Bu bir sınıf değil. Ayrıca, C ++, sınır denetimini ideal olmayan başka bir kural üzerine kurulmuştur. C ++ kılavuz ilkesi "kullanmadığınız şey için ödeme yapmaz" dır. Kodunuz doğruysa, sınır denetimine ihtiyacınız yoktur ve çalışma zamanı sınır denetiminin ek yükü için ödeme yapmak zorunda kalmamalısınız.
  • Yani C ++, std::vectorher ikisine de izin veren sınıf şablonunu sunar . operator[]verimli olacak şekilde tasarlanmıştır. Dil standardı, sınır denetimi yapmasını gerektirmez (her ikisini de yasaklamasa da). Bir vektör ayrıca sınır kontrolü gerçekleştirmesi garanti edilen at()üye fonksiyonuna sahiptir . Yani C ++ ile, bir vektör kullanırsanız her iki dünyanın da en iyisini elde edersiniz. Sınır denetimi olmadan dizi benzeri bir performans elde edersiniz ve sınır denetimli erişimi istediğiniz zaman kullanma olanağına sahip olursunuz.

5
@Jaif: Bu dizi olayını çok uzun süredir kullanıyoruz, ama yine de neden böyle basit bir hatayı kontrol etmek için test yok?
seg.server.fault

7
C ++ tasarım prensibi, eşdeğer C kodundan daha yavaş olmaması ve C'nin dizi bağlı kontrol yapmamasıydı. C tasarım prensibi, sistem programlamayı hedeflediği için temel olarak hızdı. Dizi bağlı denetimi zaman alır ve bu yapılmaz. C ++ 'daki çoğu kullanım için, yine de dizi yerine bir kap kullanmanız gerekir ve sırasıyla .at () veya [] aracılığıyla bir öğeye erişerek bağlı denetim seçimine veya sınırsız denetim seçimine sahip olabilirsiniz.
KTC

4
@seg Böyle bir çekin bir maliyeti vardır. Doğru kodu yazarsanız, bu fiyatı ödemek istemezsiniz. Bunu söyledikten sonra, tam olarak std :: vector'un kontrol edilen IS () yöntemine dönüştüm. Kullanarak ne "doğru" kod olduğunu düşündüm oldukça birkaç hata exxposed.

10
GCC'nin eski sürümlerinin aslında Emacs'ı ve bazı tanımlanmamış davranışlarla karşılaştığında Hanoi Kuleleri'nin bir simülasyonunu başlattığına inanıyorum. Dediğim gibi, her şey olabilir. ;)
jalf

4
Her şey zaten söylendi, bu sadece küçük bir zeyilname gerektiriyor. Hata ayıklama derlemeleri, bu durumlarda sürüm derlemelerine kıyasla çok affedici olabilir. Hata ayıklama bilgilerinin hata ayıklama ikili dosyalarına dahil edilmesi nedeniyle, hayati bir şeyin üzerine yazılma şansı daha azdır. Bu yüzden bazen hata ayıklama yapıları yayın derleme çökmesi sırasında iyi çalışıyor gibi görünüyor.
Zengin

31

G ++ kullanarak, komut satırı seçeneğini ekleyebilirsiniz: -fstack-protector-all.

Örneğinizde aşağıdakiler ortaya çıktı:

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

Sorunu bulmanıza veya çözmenize gerçekten yardımcı olmaz, ancak en azından segfault size bir şeylerin şeyin yanlış .


10
Daha da iyi bir seçenek buldum: -fmudflap
Hi-Angel

1
@ Hi-Angel: Modern eşdeğer, -fsanitize=addressbu hatayı hem derleme zamanında (optimize edildiyse) hem de çalışma zamanında yakalar.
Nate Eldredge

@NateEldredge +1, bugünlerde bile kullanıyorum -fsanitize=undefined,address. Ancak sınır dışı erişim dezenfektan tarafından tespit edilmediğinde std kütüphanesi ile nadir köşe vakaları olduğunu belirtmek gerekir . Bu nedenle -D_GLIBCXX_DEBUG, daha da fazla kontrol ekleyen seçeneği kullanmanızı öneririm .
Hi-Angel

12

g ++ dizi sınırlarını kontrol etmez ve 3,4 ile bir şeyin üzerine yazıyor olabilirsiniz, ancak gerçekten önemli bir şey yoktur, daha yüksek sayılarla denerseniz bir çökme elde edersiniz.

Yığının kullanılmayan kısımlarının üzerine yazıyorsunuz, yığın için ayrılan alanın sonuna ulaşıncaya kadar devam edebilirsiniz ve sonunda çökecektir

DÜZENLEME: Bununla başa çıkmanın bir yolu yok, belki bir statik kod analizörü bu hataları ortaya çıkarabilir, ancak bu çok basit, statik analizörler için bile tespit edilmemiş benzer (ancak daha karmaşık) arızalarınız olabilir


6
Eğer bundan [3] ve [4] dizilerinin adresinden nereden alırsanız, "gerçekten önemli bir şey yok" ??
namezero

7

Bildiğim kadarıyla tanımsız bir davranış. Bununla daha büyük bir program çalıştırın ve yol boyunca bir yere çökecektir. Sınır kontrolü, ham dizilerin bir parçası değildir (hatta std :: vector).

std::vector::iteratorBunun yerine std :: vector ile 's kullanın, böylece endişelenmenize gerek kalmaz.

Düzenle:

Sadece eğlenmek için, bunu çalıştırın ve kaza yapana kadar ne kadar süre kaldığını görün:

int main()
{
   int array[1];

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

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

Edit2:

Bunu yapma.

Edit3:

Tamam, diziler ve bunların işaretçilerle ilişkileri hakkında hızlı bir ders:

Dizi indeksleme kullandığınızda, gerçekten otomatik olarak kaydı kaldırılan bir işaretçi ("referans" olarak adlandırılır) kullanırsınız. Bu nedenle * (dizi [1]) yerine, dizi [1] bu değerdeki değeri otomatik olarak döndürür.

Bir diziye işaretçi varsa, şöyle:

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

Sonra ikinci bildirimde "dizi" gerçekten ilk diziye bir işaretçi çürüyor. Bu, buna eşdeğer bir davranıştır:

int *ptr = &array[0];

Tahsis ettiğinizin ötesine erişmeye çalıştığınızda, gerçekten sadece başka bir belleğe bir işaretçi kullanırsınız (hangi C ++ şikayet etmeyecektir). Yukarıdaki örnek programımı alarak, buna eşdeğerdir:

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
}

Derleyici şikayet etmeyecektir çünkü programlamada genellikle diğer programlar, özellikle işletim sistemi ile iletişim kurmanız gerekir. Bu işaretçilerle biraz yapılır.


3
Sanırım orada son örneğinizde "ptr" değerini artırmayı unuttunuz. Yanlışlıkla iyi tanımlanmış bir kod ürettiniz.
Jeff Lake

1
Haha, neden ham diziler kullanmamalısın?
jkeys

"Bu nedenle * (dizi [1]) yerine, dizi [1] bu değerdeki değeri otomatik olarak döndürür." * (Dizi [1]) öğesinin düzgün çalışacağından emin misiniz? Bence * (dizi + 1) olmalı. ps: Lol, geçmişe mesaj göndermek gibi. Ama, neyse:
muyustan

5

İpucu

Aralık hatası denetimi ile hızlı kısıtlama boyutu dizilerine sahip olmak istiyorsanız, boost :: array , (ayrıca std :: tr1 :: dizisi bundan <tr1/array>sonraki C ++ spesifikasyonunda standart konteyner olacaktır) kullanmayı deneyin . Std :: vector'dan çok daha hızlı. Yığın ya da sınıf içi örnekte, tıpkı int array [] gibi bellek ayırır.
Bu basit örnek koddur:

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

Bu program yazdırılacaktır:

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

4

C veya C ++ bir dizi erişiminin sınırlarını denetlemez.

Diziyi yığına ayırıyorsunuz. Dizinin array[3]dizine eklenmesi * ile eşdeğerdir (array + 3); burada dizi & dizisi [0] için bir göstericidir. Bu, tanımlanmamış davranışa neden olacaktır.

Bunu bazen C'de yakalamanın bir yolu , atel gibi statik bir denetleyici kullanmaktır . Eğer koşarsan:

splint +bounds array.c

üzerinde

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

    array[1] = 1;

    return 0;
}

o zaman uyarıyı alacaksınız:

array.c: (main fonksiyonunda) array.c: 5: 9: Büyük olasılıkla sınırların dışında mağaza: array [1] Kısıtlama çözülemiyor: önkoşulu karşılamak için 0> = 1 gerekir: maxSet (array @ array gerektirir) .c: 5: 9)> = 1 Bellek yazma, tahsis edilen ara belleğin ötesinde bir adrese yazabilir.


Düzeltme: İşletim sistemi veya başka bir program tarafından zaten tahsis edilmiştir. Diğer hafızanın üzerine yazıyor.
jkeys

1
"C / C ++ 'ın sınırları kontrol etmeyeceğini" söylemek tamamen doğru değildir - belirli bir uyumlu uygulamanın bunu varsayılan olarak veya bazı derleme bayrakları ile yapmasını engelleyen hiçbir şey yoktur. Sadece hiçbiri rahatsız etmiyor.
Pavel Minaev

3

Yığınınızın üzerine kesinlikle yazıyorsunuz, ancak program bunun etkilerinin fark edilmeyecek kadar basit.


2
Yığının üzerine yazılıp yazılmayacağı platforma bağlıdır.
Chris Cleeland

3

Valgrind üzerinden çalıştırın ve bir hata görebilirsiniz.

Falaina'nın belirttiği gibi, valgrind birçok yığın bozulması örneğini tespit etmez. Ben sadece valgrind altında örnek denedim ve gerçekten sıfır hata bildirir. Bununla birlikte, Valgrind diğer birçok bellek problemini bulmada etkili olabilir, eğer bulidinizi --stack-check seçeneğini içerecek şekilde değiştirmediğiniz sürece bu durumda özellikle yararlı değildir. Örneği şu şekilde oluşturup çalıştırırsanız:

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

valgrind edecek bir hata bildirir.


2
Aslında, Valgrind yığın üzerindeki hatalı dizi erişimini belirleme konusunda oldukça zayıf. (ve haklı olarak yapabileceği en iyi şey, tüm yığını geçerli bir yazma konumu olarak işaretlemektir)
Falaina

@Falaina - iyi bir nokta, ama Valgrind en azından bazı yığın hatalarını tespit edebilir.
Todd Stout

Ve valgrind kodda yanlış bir şey görmeyecektir, çünkü derleyici diziyi optimize edecek ve basit bir 3 ve 4 çıktısı alacak kadar akıllıdır. have gösterilmez.
Goswin von Brederlow

2

Tanımsız davranış sizin lehinize çalışıyor. Görünüşe göre, hafızan ne olursa olsun, önemli bir şey tutmuyor. C ve C ++ 'ın dizileri kontrol etmediğini unutmayın, bu yüzden böyle şeyler derleme veya çalışma zamanında yakalanmayacaktır.


5
Hayır, Tanımlanamayan davranış temiz bir şekilde çöktüğünde "sizin lehinize çalışır". Çalışıyor gibi göründüğünde, bu mümkün olan en kötü senaryo ile ilgilidir.
jalf

@JohnBode: Eğer jalf yorumuna göre ifadeleri düzeltmek daha iyi olurdu
Yıkıcı

1

Diziyi ile başlattığınızda, int array[2]2 tamsayı için alan ayrılır; ancak tanımlayıcı arrayyalnızca bu alanın başlangıcını gösterir. Daha sonra array[3]ve array[4]öğelerine eriştiğinizde , derleyici, diziyi yeterince uzunsa, bu adreslerin bu değerlerin nerede olacağını gösterecek şekilde artırır; array[42]önce başlatmadan bir şeye erişmeye çalıştığınızda , o konumda zaten bellekte olan değeri elde edersiniz.

Düzenle:

İşaretçiler / diziler hakkında daha fazla bilgi: http://home.netcom.com/~tjensen/ptr/pointers.htm


0

int dizi [2] bildirdiğinizde; her biri 4 baytlık 2 bellek alanı ayırırsınız (32bit program). kodunuza dizi [4] yazarsanız, yine de geçerli bir çağrıya karşılık gelir, ancak yalnızca çalışma zamanında işlenmemiş bir istisna atar. C ++ manuel bellek yönetimi kullanır. Bu aslında programları hacklemek için kullanılan bir güvenlik açığıdır

bu anlamada yardımcı olabilir:

int * somepointer;

somepointer [0] = somepointer [5];


0

Anladığım kadarıyla, yerel değişkenler yığına tahsis edilir, bu nedenle kendi yığınınızdaki sınırların dışına çıkmak, oob çok fazla gitmez ve yığın boyutunuzu aşmazsanız, yalnızca diğer bazı yerel değişkenlerin üzerine yazabilir. İşlevinizde bildirilmiş başka bir değişkeniniz olmadığından, herhangi bir yan etkiye neden olmaz. İlkinden hemen sonra başka bir değişken / dizi bildirmeyi deneyin ve onunla ne olacağını görün.


0

C 'dizisi [index]' yazdığınızda, onu makine yönergelerine çevirir.

Çeviri şu şekildedir:

  1. 'dizinin adresini al'
  2. 'dizinin oluşturduğu nesne türünün boyutunu al'
  3. 'türün boyutunu dizinle çarpma'
  4. 'sonucu dizinin adresine ekle'
  5. 'ortaya çıkan adreste ne var oku'

Sonuç, dizinin bir parçası olan ya da olmayan bir şeyi ele alır. Makine talimatlarının çarpıcı hızına karşılık, sizin için işleri kontrol eden bilgisayarın güvenlik ağını kaybedersiniz. Eğer titiz ve dikkatli iseniz sorun değil. Eğer özensiz ya da bir hata yaparsanız yanarsınız. Bazen istisnaya neden olan geçersiz bir talimat oluşturabilir, bazen buna neden olmayabilir.


0

Sık sık gördüğüm ve aslında kullanılmıştı hoş bir yaklaşım uint THIS_IS_INFINITY = 82862863263;dizinin sonunda bazı NULL türü eleman (veya oluşturulan bir gibi ) enjekte etmektir.

Sonra döngü koşulu kontrolünde, TYPE *pagesWordsbir tür işaretçi dizisidir:

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

Dizi çözümlerle doldurulursa bu çözüm sözcük oluşturmaz struct.


0

Şimdi soruda belirtildiği gibi std :: vector :: at kullanarak sorunu çözecek ve erişmeden önce ilişkili bir kontrol yapacak.

İlk kodunuz olarak yığında bulunan sabit boyutlu bir diziye ihtiyacınız varsa C ++ 11 new container std :: array; vektör olarak fonksiyonda std :: array :: vardır. Aslında işlev, anlam ifade ettiği tüm standart kaplarda, yani işleç [] tanımlandığı yerdedir :( deque, map, unordered_map), std :: bitset olarak adlandırıldığı std :: bitset hariç: :Ölçek.


0

gcc'nin bir parçası olan libstdc ++ hata denetimi için özel bir hata ayıklama moduna sahiptir . Derleyici bayrağı ile etkinleştirilir -D_GLIBCXX_DEBUG. Diğer şeylerin yanı sıra std::vectorperformans maliyetiyle kontrol etmeyi de sınırlar . İşte gcc son sürümü ile çevrimiçi demo .

Yani aslında libstdc ++ hata ayıklama modu ile sınır kontrolü yapabilirsiniz, ancak normal libstdc ++ moduna kıyasla dikkate değer bir performansa mal olduğu için sadece test ederken yapmalısınız.


0

Programınızı biraz değiştirirseniz:

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

(Büyük harf değişiklikleri - bunu deneyecekseniz küçük harflerle yazın.)

Foo değişkeninin çöpe atıldığını göreceksiniz . Kodunuz edecek varolmayan dizide [3] ve dizinin [4] içine değerlerini saklamak ve bunları düzgün almak mümkün, ama kullanılan gerçek depolama dan olacak foo .

Böylece, orijinal örneğinizde dizinin sınırlarını aşarak "kaçabilirsiniz", ancak başka bir yerde hasar verme pahasına - teşhis edilmesi çok zor olabilen hasar .

Neden otomatik sınır kontrolü yoktur? Doğru yazılmış bir programın buna ihtiyacı yoktur. Bu yapıldıktan sonra, çalışma zamanı sınırlarını kontrol etmek ve yapmak için hiçbir neden yoktur ve bunu yapmak programı yavaşlatır. Tasarım ve kodlama sırasında tüm bunları anlamak için en iyisi.

C ++, montaj diline olabildiğince yakın olacak şekilde tasarlanmış C'yi temel alır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.