범위를 벗어난 배열에 액세스하면 오류가 발생하지 않습니다. 왜 그렇습니까?


177

다음과 같이 C ++ 프로그램에서 범위를 벗어난 값을 할당합니다.

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

이 프로그램은 인쇄 34. 가능하지 않아야합니다. g ++ 4.3.3을 사용하고 있습니다.

다음은 컴파일 및 실행 명령입니다

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

할당 할 때만 array[3000]=3000세그먼트 오류가 발생합니다.

gcc가 배열 범위를 확인하지 않으면 나중에 심각한 문제가 발생할 수 있으므로 프로그램이 올바른지 어떻게 확인할 수 있습니까?

위의 코드를

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

이것도 오류가 없습니다.



16
물론이 코드는 버그가 있지만 정의되지 않은 동작을 생성 합니다. 정의되지 않음은 완료 될 수도 있고 실행되지 않을 수도 있음을 의미합니다. 충돌이 보장되지 않습니다.
dmckee ---

4
원시 배열로 나사를 조이지 않아도 프로그램이 올바른지 확인할 수 있습니다. C ++ 프로그래머는 임베디드 / OS 프로그래밍을 제외하고 컨테이너 클래스를 대신 사용해야합니다. 사용자 컨테이너에 대한 이유로이 내용을 읽으십시오. parashift.com/c++-faq-lite/containers.html
jkeys

8
벡터는 반드시 []를 사용하여 범위를 검사 할 필요는 없습니다. .at ()를 사용하면 []와 동일한 기능을 수행하지만 범위 검사를 수행합니다.
David Thornley

4
범위를 벗어난 요소에 액세스 할 때 A vector 자동 크기 조정 되지 않습니다 ! 그것은 단지 UB입니다!
Pavel Minaev

답변:


364

모든 C / C ++ 프로그래머의 가장 친한 친구 Undefined Behavior에 오신 것을 환영합니다 .

여러 가지 이유로 언어 표준에 의해 지정되지 않은 것이 많이 있습니다. 이것은 그들 중 하나입니다.

일반적으로 정의되지 않은 동작이 발생할 때마다 어떤 일 이 발생할 수 있습니다. 응용 프로그램이 중단되거나 정지되거나 CD-ROM 드라이브를 꺼내거나 코에서 악마가 나오게 할 수 있습니다. 하드 드라이브를 포맷하거나 모든 포르노를 할머니에게 이메일로 보낼 수 있습니다.

운이 좋지 않으면 제대로 작동하는 것처럼 보일 수도 있습니다 .

이 언어는 단순히 배열 경계 안에 있는 요소에 액세스하면 어떻게되는지 알려줍니다 . 범위를 벗어나면 어떤 일이 발생하는지 정의되지 않은 상태로 남아 있습니다. 그것은 수있는 컴파일러에 오늘 작업에하지만 법적 C 또는 C ++하지 않고, 여전히 프로그램을 실행할 때 작동합니다 않는다고 보장 할 수는 없습니다. 아니면 지금도하지 덮어 중요한 데이터를 가지고, 당신은 그냥 것으로, 문제가 발생하지 않은 것으로 되어 원인에가는 - 아직.

에 관해서는 이유를 확인 끝이 없다, 대답에 몇 가지 측면이있다 :

  • 배열은 C에서 남은 것입니다. C 배열은 가능한 한 원시적입니다. 연속적인 주소를 가진 일련의 요소들. 단순히 원시 메모리를 노출하기 때문에 경계 검사가 없습니다. C에서 강력한 경계 검사 메커니즘을 구현하는 것은 거의 불가능했을 것입니다.
  • C ++에서는 클래스 유형에 대한 경계 검사가 가능합니다. 그러나 배열은 여전히 ​​일반적인 C 호환 가능 배열입니다. 수업이 아닙니다. 또한 C ++은 경계 검사를 비 이상적으로 만드는 또 다른 규칙을 기반으로합니다. C ++ 안내 원칙은 "사용하지 않는 것에 대해서는 비용을 지불하지 않는다"는 것입니다. 코드가 올바른 경우 경계 검사가 필요하지 않으며 런타임 경계 검사의 오버 헤드에 대해 비용을 지불하지 않아도됩니다.
  • 따라서 C ++은 std::vector클래스 템플릿을 제공하므로 둘 다 허용합니다. operator[]효율적으로 설계되었습니다. 언어 표준에서는 경계 검사를 수행 할 필요가 없습니다 (물론 금지하지는 않지만). 벡터는 또한 보유 at()부재 함수 보장 한계 검사를 수행한다. 따라서 C ++에서는 벡터를 사용하면 두 세계를 모두 활용할 수 있습니다. 당신은 배열과 같은 경계 검사없이 성능을, 그리고 당신이 그것을 할 때 사용 경계 - 검사 액세스 할 수있는 기능을 얻을.

5
@Jaif : 우리는이 배열을 오랫동안 사용 해 왔지만 여전히 간단한 오류를 검사하는 테스트가없는 이유는 무엇입니까?
seg.server.fault

7
C ++ 디자인 원칙은 동등한 C 코드보다 느려서는 안되며 C는 배열 바운드 검사를 수행하지 않는다는 것입니다. C 디자인 원칙은 기본적으로 시스템 프로그래밍을위한 속도였습니다. 배열 바운드 검사에는 시간이 걸리므로 수행되지 않습니다. C ++에서 대부분의 경우 어쨌든 배열 대신 컨테이너를 사용해야하며 .at () 또는 []를 통해 요소에 각각 액세스하여 바운드 검사 또는 바운드 검사를 선택할 수 있습니다.
KTC

4
@seg 이러한 검사에는 비용이 듭니다. 올바른 코드를 작성하면 그 가격을 지불하고 싶지 않습니다. 말했듯이, 나는 std :: vector 's at () 메소드로 완전히 변환되었습니다. 이 코드를 사용하면 "정확한"코드라고 생각했던 오류가 상당 부분 노출되었습니다.

10
구 버전의 GCC는 실제로 특정 유형의 정의되지 않은 동작이 발생했을 때 실제로 Emacs와 하노이 타워 시뮬레이션을 시작했다고 생각합니다. 내가 말했듯이, 어떤 일이 일어날 수 있습니다. ;)
jalf

4
모든 것이 이미 언급되었으므로 작은 부록 만 보증합니다. 이러한 상황에서는 릴리스 빌드와 비교할 때 디버그 빌드가 매우 관대 할 수 있습니다. 디버그 바이너리에 디버그 정보가 포함되어 있기 때문에 중요한 내용을 덮어 쓸 가능성이 적습니다. 그렇기 때문에 릴리스 빌드가 충돌하는 동안 디버그 빌드가 제대로 작동하는 것 같습니다.
Rich

31

g ++를 사용하여 명령 행 옵션을 추가 할 수 있습니다 -fstack-protector-all.

귀하의 예에서 다음과 같은 결과가 나타났습니다.

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

실제로 문제를 찾거나 해결하는 데 도움이되지는 않지만 적어도 segfault는 무언가 잘못 되었음을 알려줍니다 .


10
: 난 그냥 심지어 더 나은 옵션을 찾을 -fmudflap
하이 천사를

1
@ Hi-Angel : 최신 버전은 -fsanitize=address컴파일 타임 (최적화시)과 런타임에이 버그를 포착합니다.
Nate Eldredge

@NateEldredge +1, 요즘에는 심지어 사용 -fsanitize=undefined,address합니다. 그러나 경계를 벗어난 액세스가 sanitizer에 의해 감지되지 않는 경우 std 라이브러리에 드문 코너 사례 가 있음을 주목할 가치가 있습니다. 이러한 이유로 -D_GLIBCXX_DEBUG옵션 을 추가로 사용하는 것이 좋습니다 .
안녕 천사

12

g ++은 배열 범위를 확인하지 않으며 3, 4로 무언가를 덮어 쓸 수 있지만 실제로는 중요하지 않습니다. 높은 숫자로 시도하면 충돌이 발생합니다.

사용되지 않은 스택의 일부만 덮어 쓰고 있습니다. 스택에 할당 된 공간의 끝에 도달 할 때까지 계속하면 결국 충돌이 발생합니다.

편집 : 당신은 그것을 처리 할 수있는 방법이 없습니다. 정적 코드 분석기는 이러한 오류를 나타낼 수 있지만 너무 간단합니다. 정적 분석기에서도 비슷한 (그러나 더 복잡한) 오류가 감지되지 않을 수 있습니다


6
array [3] 및 array [4]의 주소에서 그로부터 어디에서 얻을 수 있습니까? "정말 중요한 것은 없습니다"??
namezero

7

내가 아는 한 정의되지 않은 동작입니다. 그것으로 더 큰 프로그램을 실행하면 도중에 충돌 할 것입니다. 바운드 검사는 원시 배열 (또는 std :: vector)의 일부가 아닙니다.

대신 std :: vector을 사용 std::vector::iterator하면 걱정할 필요가 없습니다.

편집하다:

재미를 위해 이것을 실행하고 충돌 할 때까지 얼마나 걸리는지 확인하십시오.

int main()
{
   int array[1];

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

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

편집 2 :

그것을 실행하지 마십시오.

편집 3 :

여기 배열과 포인터와의 관계에 대한 간단한 교훈이 있습니다.

배열 인덱싱을 사용하는 경우 실제로 자동으로 역 참조되는 변장 ( "참조")의 포인터를 사용합니다. 이것이 * (array [1]) 대신 array [1]이 해당 값의 값을 자동으로 반환하는 이유입니다.

다음과 같이 배열에 대한 포인터가있는 경우

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

그런 다음 두 번째 선언의 "배열"은 실제로 첫 번째 배열에 대한 포인터로 쇠퇴합니다. 이것은 이것과 동등한 동작입니다 :

int *ptr = &array[0];

할당 한 것 이상으로 액세스하려고하면 실제로 다른 메모리 (C ++에서 불평하지 않는)에 대한 포인터를 사용하고 있습니다. 위의 예제 프로그램을 사용하면 다음과 같습니다.

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
}

프로그래밍에서 종종 다른 프로그램, 특히 운영 체제와 통신해야하기 때문에 컴파일러가 불평하지 않습니다. 이것은 포인터로 꽤 이루어집니다.


3
마지막 예제에서 "ptr"을 늘리는 것을 잊었다 고 생각합니다. 실수로 잘 정의 된 코드를 생성했습니다.
Jeff Lake

1
하하, 왜 원시 배열을 사용하지 않아야하는지 알아?
jkeys

"그래서 * (array [1]) 대신 array [1]이 해당 값의 값을 자동으로 반환합니다." * (array [1])이 제대로 작동합니까? 나는 그것이 * (배열 + 1)이어야한다고 생각합니다. ps : Lol, 과거에 메시지를 보내는 것과 같습니다. 그러나, 어쨌든 :
muyustan

5

힌트

당신이 범위 에러 체크와 빠른 구속 크기의 배열을 가지고 싶다면, 사용하려고 부스트 : : 배열을 (또한, 표준 : TR1 :: 배열 에서 <tr1/array>그 다음에 C ++ 사양 표준 컨테이너 될 것입니다). std :: vector보다 훨씬 빠릅니다. int array []처럼 힙이나 클래스 인스턴스 내부의 메모리를 예약합니다.
이것은 간단한 샘플 코드입니다.

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

이 프로그램은 다음을 인쇄합니다 :

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

4

C 또는 C ++는 배열 액세스의 경계를 확인하지 않습니다.

스택에 배열을 할당하고 있습니다. via를 인덱싱 array[3]하는 것은 *와 동일합니다 (array + 3). 여기서 array는 & array [0]에 대한 포인터입니다. 정의되지 않은 동작이 발생합니다.

때로는 C에서 이것을 포착하는 한 가지 방법 은 splint 와 같은 정적 검사기를 사용하는 것 입니다. 당신이 실행하는 경우 :

splint +bounds array.c

의 위에,

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

    array[1] = 1;

    return 0;
}

그러면 경고가 나타납니다.

array.c : (함수 메인에 있음) array.c : 5 : 9 : 범위를 벗어 났을 가능성이있는 저장소 : array [1] 제약 조건을 해결할 수 없음 : 요구 사항을 충족하는 데 필요한 0> = 1 필요 : maxSet (array @ array) .c : 5 : 9)> = 1 메모리 쓰기는 할당 된 버퍼 이외의 주소에 쓸 수 있습니다.


수정 : 이미 OS 또는 다른 프로그램에 의해 할당되었습니다. 그는 다른 기억을 덮어 쓰고있다.
jkeys

1
"C / C ++에서 범위를 검사하지 않습니다"라고 말하는 것이 완전히 정확하지는 않습니다. 기본적으로 또는 일부 컴파일 플래그를 사용하여 특정 호환 구현을 수행하는 것을 배제하는 것은 없습니다. 그들 중 아무도 귀찮게하지 않습니다.
Pavel Minaev

3

확실히 스택을 덮어 쓰고 있지만 프로그램의 영향은 눈에 띄지 않습니다.


2
스택의 덮어 쓰기 여부는 플랫폼에 따라 다릅니다.
Chris Cleeland

3

Valgrind를 통해 이것을 실행 하면 오류가 표시 될 수 있습니다.

Falaina가 지적했듯이 valgrind는 많은 스택 손상 사례를 감지하지 못합니다. 방금 valgrind에서 샘플을 시험해 보았고 실제로 제로 오류를보고하지 않습니다. 그러나 Valgrind는 많은 다른 유형의 메모리 문제를 찾는 데 도움이 될 수 있습니다. --stack-check 옵션을 포함하도록 bulid를 수정하지 않는 한이 경우에는 특히 유용하지 않습니다. 다음과 같이 샘플을 빌드하고 실행하면

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

valgrind 오류 보고합니다.


2
실제로 Valgrind는 스택에서 잘못된 배열 액세스를 결정하는 데 매우 취약합니다. (그리고 당연히 최선의 방법은 전체 스택을 유효한 쓰기 위치로 표시하는 것입니다)
Falaina

@Falaina-좋은 지적이지만 Valgrind는 적어도 일부 스택 오류를 감지 할 수 있습니다.
토드 스타우트

valgrind는 컴파일러가 배열을 최적화하고 리터럴 3과 4를 출력하기에 충분히 영리하기 때문에 코드에 아무런 문제가 없습니다. gcc가 배열 경계를 확인하기 전에 최적화가 이루어집니다. 가 표시되지 않았습니다.
Goswin von Brederlow

2

정의되지 않은 행동이 유리합니다. 어떤 메모리를 방해하든간에 중요한 것은 없습니다. C와 C ++는 배열에 대한 경계 검사를 수행하지 않으므로 컴파일이나 런타임에 이와 같은 것들이 포착되지 않습니다.


5
아니요, 정의되지 않은 동작은 깔끔하게 충돌 할 때 "선호합니다". 작동하는 것처럼 보이면 최악의 시나리오에 관한 것입니다.
jalf

@JohnBode : 그렇다면 jalf의 의견에 따라 문구를 수정하면 더 좋을 것입니다.
Destructor

1

을 사용하여 배열을 초기화하면 int array[2]2 개의 정수를위한 공간이 할당됩니다. 그러나 식별자는 array단순히 해당 공간의 시작을 가리 킵니다. 그런 다음 array[3]and array[4]에 액세스 하면 컴파일러는 배열이 충분히 긴 경우 해당 주소를 증분하여 해당 값이있는 위치를 가리 킵니다. array[42]먼저 초기화하지 않고 무언가에 액세스 하면 해당 위치의 메모리에 이미있는 값이 무엇이든 얻을 수 있습니다.

편집하다:

포인터 / 배열에 대한 추가 정보 : http://home.netcom.com/~tjensen/ptr/pointers.htm


0

int array를 선언 할 때 [2]; 각각 4 바이트 (32 비트 프로그램)의 메모리 공간 2 개를 예약합니다. 코드에 array [4]를 입력하면 여전히 유효한 호출에 해당하지만 런타임에만 처리되지 않은 예외가 발생합니다. C ++는 수동 메모리 관리를 사용합니다. 이것은 실제로 해킹 프로그램에 사용 된 보안 결함입니다

이것은 이해하는 데 도움이 될 수 있습니다.

int * somepointer;

somepointer [0] = somepointer [5];


0

내가 이해하는 것처럼, 로컬 변수는 스택에 할당되므로 너무 많이 들이지 않고 스택 크기를 초과하지 않으면 자신의 스택에서 범위를 벗어나면 다른 로컬 변수를 덮어 쓸 수 있습니다. 함수에 선언 된 다른 변수가 없으므로 부작용이 발생하지 않습니다. 첫 번째 변수 바로 다음에 다른 변수 / 배열을 선언하고 그 결과가 어떻게되는지 확인하십시오.


0

C로 'array [index]'를 쓰면 기계 명령어로 변환됩니다.

번역은 다음과 같습니다.

  1. '배열의 주소를 얻습니다'
  2. '배열이 구성된 객체 유형의 크기를 얻습니다'
  3. '타입의 크기에 인덱스를 곱하십시오'
  4. '결과를 배열 주소에 추가하십시오'
  5. '결과 주소에있는 내용 읽기'

결과는 배열의 일부이거나 아닐 수있는 것을 처리합니다. 기계 설명서의 빠른 속도와 교환하여 컴퓨터의 안전망을 잃어 버립니다. 세심하고주의를 기울이면 문제가되지 않습니다. 실수를하거나 실수를하면 화상을 입을 수 있습니다. 때로는 예외를 유발하는 잘못된 명령을 생성 할 수도 있지만 그렇지 않은 경우도 있습니다.


0

내가 자주 보았고 실제로 사용 된 좋은 접근 방식 uint THIS_IS_INFINITY = 82862863263;은 배열 끝에 NULL 유형 요소 (또는 생성 된 요소)를 주입하는 것 입니다.

그런 다음 루프 조건 검사에서 TYPE *pagesWords일종의 포인터 배열이 있습니다.

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

이 솔루션은 배열이 struct유형 으로 채워져 있으면 말을하지 않습니다 .


0

std :: vector :: at 사용하여 질문에서 언급했듯이 액세스하기 전에 문제를 해결하고 바운드 검사를 수행합니다.

첫 번째 코드로 스택에있는 일정한 크기의 배열이 필요한 경우 C ++ 11 새 컨테이너 std :: array; 벡터로 std :: array :: at 함수가 있습니다. 실제로이 함수는 의미가있는 모든 표준 컨테이너에 존재합니다. 즉, operator []가 정의 된 곳에서 std :: bitset을 제외하고 std :: bitset을 제외하고 : (deque, map, unorder_map) : :테스트.


0

gcc의 일부인 libstdc ++에는 오류 검사를위한 특수 디버그 모드 가 있습니다. 컴파일러 플래그로 활성화됩니다 -D_GLIBCXX_DEBUG. 무엇보다도 std::vector성능 비용으로 검사를 수행합니다. 여기 온라인 데모있습니다 최신 버전의 gcc 입니다.

따라서 실제로 libstdc ++ 디버그 모드를 사용하여 범위 검사를 수행 할 수 있지만 일반 libstdc ++ 모드와 비교하여 현저한 성능 비용이 들기 때문에 테스트 할 때만 수행해야합니다.


0

프로그램을 약간 변경 한 경우 :

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

(자본 변경-시도하면 소문자로 변경하십시오.)

foo 변수 가 휴지통에 있음을 알 수 있습니다. 코드 존재하지 않는 array [3] 및 array [4]에 값을 저장하고 올바르게 검색 할 수 있지만 실제 사용 된 스토리지는 foo 에서 가져옵니다. 입니다.

따라서 원래 예제에서 어레이의 범위를 초과하여 "피할 수는 있지만" 진단을하기 매우 어려운 다른 곳에서 손상을 일으킬 수 있습니다 .

자동 경계 검사가없는 이유에 대해서는 올바르게 작성된 프로그램이 필요하지 않습니다. 일단 완료되면 런타임 경계 검사를 수행 할 이유가 없으며 그렇게하면 프로그램 속도가 느려질 것입니다. 디자인과 코딩 과정에서 모든 것을 알아내는 것이 가장 좋습니다.

C ++는 C를 기반으로하며 가능한 한 어셈블리 언어에 가깝게 설계되었습니다.

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