ProgramingTip

정의되지 않은 동작 밖에서 전역 배열에 액세스하고 동작합니까?

bestdevel 2020. 12. 4. 19:47
반응형

정의되지 않은 동작 밖에서 전역 배열에 액세스하고 동작합니까?


저는 오늘 수업에서 시험을 보았습니다 .--- C 코드와 입력을 읽고 필요한 답은 프로그램이 실제로 실행 화면에 내용 내용을 실행했습니다. a[4][4]전역 변수로 선언되지 않은 질문 중 하나가 해당 프로그램의 지점에서 액세스를 시도 a[27][27]하는 " 범위를 벗어난 배열에 액세스하는 범위를 벗어난 동작입니다. "라고 답했지만 교사는 a[27][27]값이 0.

그 후 "초기화되지 않은 모든 골발 변수가 "로 설정되어 있습니다 "참가 인지 아닌지 확인하기 위해 몇 가지 코드시도 했습니다0 . 음, 사실 인 것 같습니다.

이제 내 질문 :

  • 추가 메모리가 지워지고 코드 실행을 위해 예약 된 것입니다. 얼마나 많은 메모리가 예약되어 있습니까? 컴파일러가 필요한 것보다 더 많은 메모리를 예약하는 이유는 무엇이며 그 용도는 무엇입니까?
  • a[27][27]0모든 환경에?

편집하다 :

이 코드에서는 a[4][4]IS 단지 전역 변수에서 좀 더 지역의 사람이 main()있습니다.

DevC ++에서 해당 코드를 다시 시도 했습니다 . 그들 모두는입니다 0. 그러나 Vyktor 가 지적한 것처럼 VSE에서는 거의 대부분의 가치가 있습니다.0


: 그것은 정의되지 않은 행동이며 항상 생산하는 셀 수는 없습니다 0.

이 경우 0이 표시되는 이유는 최신 운영 유효성이 표시되는 변수보다 훨씬 큰 페이지 (x86에서 최소 4KB)라는 최후의 처리로 청크의 프로세스에 메모리를 할당합니다. 단일 변수가 있으면 페이지의 어딘가에 위치합니다. a가 유형 int[][]이고 ints가 시스템에서 4 바이트 라고 가정하면 a[27][27]시작 부분에서 약 500 바이트에 위치합니다 a. 따라서 a페이지 시작 부분에있는 한 액세스 a[27][27]는 실제 메모리에 의해 뒷받침이 있고, 읽는 페이지 오류가 위반을 일으키지.

물론, 당신은 믿을 수 없습니다. 예를 들어, a거의 4KB의 다른 전역 변수 a[27][27]가 존재하는 경우 메모리가 지원되지 않습니다.

프로세스가 충돌하지 않는 것이 좋습니다 0. 이 변수를 할당하고 해당 값을 인쇄하는 것 외에는 아무 작업도 수행하지 않는 최신 다중 사용자 운영 체제에서 매우 간단한 프로그램이있는 경우 0. 운영 유지는 한 프로세스 또는 사용자의 민감한 데이터가 다른 프로세스로 유출되지 않도록 프로세스에 메모리를 넘길 때 내용을의 무해한 값 (일반적으로 모두 0)으로 설정합니다.

그러나 읽는 임의의 메모리가 0이 아닙니다. 마지막 사용시 메모리가 초기화되지 않은 플랫폼에서 프로그램을 사용할 수 있고 마지막 사용에서 값이 무엇인지 확인할 수 있습니다.

또한 a0 아닌 값으로이 초기화되는 다른 전역 변수가 충분히 뒤 6 오는 경우 액세스 a[27][27]하면 어떤 값이 발생하는지-display됩니다.


아웃 바운드 어레이 액세스는 결과 때문에 결과 예측할 수단 정의 문제이며, a[27][27]존재가 0전혀 수 없습니다.

clang우리가 다음을 사용하면 매우 명확하게 알려줍니다 -fsanitize=undefined.

runtime error: index 27 out of bounds for type 'int [4][4]'

컴파일러는 정말 전혀 전혀 아무것도 할 수없는 정의되지 않은 동작이 있고, 우리는 심지어 예를 본 gcc무한 루프에 유한 루프를 설정하지 않은 것이 해결 최적화를 기반으로합니다. 정의되지 않은 동작을 감지하면 둘 다 clang및 일부 gcc상황에서 정의되지 않은 명령이 생성 될 수 있습니다 .

정의되지 않은 동작 인 이유 , 범위를 벗어난 포인터 산술이 정의되지 않은 동작 인 이유는 무엇입니까? 이유에 대한 좋은 요약을 제공합니다. 예를 들어 결과 포인터가 유효한 주소가 아닐 수 있고 포인터 할가 할당 된 메모리 페이지 외부를 가리킬 수 있고 RAM 대신 메모리 매핑 하드웨어로 작업 수 있습니다.

대부분의 경우 정적 변수가 저장되는 세그먼트는 할당되는 배열 또는 스왑되는 세그먼트보다 훨씬 더 크지 만, 완전히 톰의 경우 운이 좋게됩니다. 대부분의 경우 페이지 크기가 4k 이고 액세스 a[27][27]가 해당 범위가 아니므로 세분화 오류가 표시되지 않습니다.

표준이 말하는 것

초안 C99 표준은 이 섹션에서 정의되지 않은 동작입니다 말해 6.5.6 첨가제 것 커버가 배열 액세스가 내려 오는 인 연산 포인터입니다. 그것은 말한다 :

정수 유형이있는 배열이 포인터에 더해 지거나 뺄 때 결과는 포인터 피연산자의 유형을 갖습니다. 포인터 피 연산 배열 객체의 요소를 가리키고 배열이 충분히 크면 결과는 결과와 원래 배열 요소의 첨자의 차이가 정수와 같도록 요소에서 배열이 있습니다.

[...]

포인터 피연산자와 결과가 동일한 배열 개체의 요소를 가리 키거나 배열 개체의 마지막 요소를 지나는 요소를 가리키면 평가에서 오버플로가 발생하지 않습니다. 표준화 된 동작이 정의되지 않습니다. 결과가 배열되지 않았던 마지막 요소를 하나 지나면 평가되는 단 * 연산자의 피 연산 결과.

정의되지 않은 동작의 표준 정의는 표준이 동작에 대한 요구 사항을 부과하지 않을 가능성이있는 동작은 예측 수 없음을 알려줍니다.

이 국제 표준이 요구 사항을 부과하지 않는 비 이식 적이거나 오류가있는 프로그램 구성 또는 잘못된 데이터를 사용할 때의 행동

참고 가능한 정의되지 않은 동작은 예측할 수없는 결과로 상황을 완전히 무시하는 것에서 [...]


다음은 정의되지 않은 동작을 지정하는 표준 인용문입니다.

J.2 정의되지 않은 동작

  • 주어진 첨자로 선언에 접근 할 수있는 경우에도 배열 첨저 범위를 벗어납니다 (int a [4] [5]) (6.5.6)로 선언 된 lvalue에서 a [1] [7]에서와 같이.

  • 배열과 정수 유형으로 또는 그 너머에 포인터를 더하거나 빼면 배열 배열 바로 너머를 선언 결과가 생성되는 평가 단항 * 연산자의 피연산자로 사용됩니다 (6.5.6).

귀하의 경우 배열 첨자는 완전히 외부에 있습니다. 0이 장착되어 있습니다.

또한 전체 프로그램의 동작이 문제가됩니다.


Visual Studio 2012에서 코드를 실행하고 다음과 같은 결과를 얻은 경우 (실행할 때마다 다름) :

Address of a: 00FB8130
Address of a[4][4]: 00FB8180
Address of a[27][27]: 00FB834C
Value of a[27][27]: 0
Address of a[1000][1000]: 00FBCF50
Value of a[1000][1000]: <<< Unhandled exception at 0x00FB3D8F in GlobalArray.exe:
                            0xC0000005: Access violation reading location 0x00FBCF50.

당신이 볼 때 모듈 창 당신은 당신의 응용 프로그램 모듈 메모리 범위는 것을 알 수 00FA0000-00FBC000있습니다. 그리고, 당신은하지 않는 CRT 점검 켜져 아무것도 당신이 내부합니까 무엇을 제어 할 메모리 (이 위반하지 당신 않는 한 같은 메모리 보호 ).

가지고 당신은 그래서 0에서 a[27][27]우연히 순수. 00FB8130( a) 위치에서 메모리보기를 열면 다음과 같은 내용이 표시 될 것입니다.

0x00FB8130  08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8140  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8150  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8160  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8170  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8180  01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00  ................
0x00FB8190  c0 90 45 00 b0 e9 45 00 00 00 00 00 00 00 00 00  À.E.°éE.........
0x00FB81A0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB81B0  00 00 00 00 80 5c af 0f 00 00 00 00 00 00 00 00  ....€\¯.........
0x00FB81C0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
.......... 
0x00FB8330  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8340  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ <<<<
0x00FB8350  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
..........                                      ^^ ^^ ^^ ^^

컴파일러를 0사용하면 메모리를 사용하는 방식 때문에 항상 해당 코드를 사용할 수 있고 몇 바이트 밖에 있지 않은 여러 변수를 사용할 수 있습니다.

예를 들어 위에있는 메모리 는의 정수 값을 포함 a[6][0]하는 주소 0x00FB8190가리 킵니다 4559040.


그런 다음 교사 에게이 문제를 설명하십시오.

이것이 당신의 시스템에서 작동하는지 모르겠지만 0이 아닌 a바이트를 가진 배열 a[27][27].

의 나는 내용을 인쇄 내 시스템에 a[27][27]그것을했다 0xFFFFFFFF. 즉, unsigned로 변환 된 -1은 2의 보수로 전체 비트입니다.

#include <stdio.h>
#include <string.h>

#define printer(expr) { printf(#expr" = %u\n", expr); }

   unsigned int d[8096];
   int a[4][4];  /* assuming an int is 4 bytes, next 4 x 4 x 4 bytes will be initialised to zero */
   unsigned int b[8096];
   unsigned int c[8096];


int main() {

   /* make sure next bytes do not contain zero'd bytes */
   memset(b, -1, 8096*4);
   memset(c, -1, 8096*4);
   memset(d, -1, 8096*4);

   /* lets check normal access */
   printer(a[0][0]);
   printer(a[3][3]);

   /* Now we disrepect the machine - undefined behaviour shall result */
   printer(a[27][27]);

   return 0;
}

이것은 내 결과입니다.

a[0][0] = 0
a[3][3] = 0
a[27][27] = 4294967295

Visual Studio에서 메모리보기에 대한 의견을 보았습니다. 가장 쉬운 방법은 코드 어딘가에 중단 점을 추가 (실행 중지) 한 다음 디버그 ... 창으로 이동하는 것입니다. 메모리 메뉴에서 예를 들어 메모리 1을 선택합니다. 그런 다음 배열의 메모리 주소를 찾습니다 a. 제 경우 주소는 0x0130EFC0. 그래서 당신 0x0130EFC0은 주소를 입력하고 Enter를 누르십시오. 이것은 그 위치의 메모리를 보여줍니다.

예 : 내 경우.

0x0130EFC0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ..................................
0x0130EFE2  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff  ..............................ÿÿÿÿ
0x0130F004  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 
0x0130F026  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
0x0130F048  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

물론 0은 바이트 크기가 4 x 4 x sizeofan int (내 경우 4) = 64 바이트 인 배열 a 입니다. 주소 0x0130EFC0의 바이트는 각각 0xFF입니다 (b, c 또는 d 내용에서).

참고 :

0x130EFC0 + 64 = 0x130EFC0 + 0x40 = 130F000

그것은 ff당신이 보는 모든 바이트 의 시작입니다 . 아마도 array b.


일반적인 컴파일러의 경우 범위를 넘어서 배열에 액세스하면 매우 특별한 경우에만 예측 가능한 결과를 얻을 수 있으며 이에 의존해서는 안됩니다. 예 :

int a[4][4];
int b[4][4];

정렬 문제가없고 적극적인 최적화 나 살균 검사를 요구하지 않는다면 a[6][1]실제로는 b[2][1]. 그러나 프로덕션 코드에서는 절대로하지 마십시오!


A의 특정 시스템, 선생님은 올바른있을 수 있습니다 - 그것은 특정 컴파일러와 운영 체제가 작동 얼마나 할 수있다.

A의 일반적인 ( "내부자"지식없이 IE) 시스템 다음 대답은 올바른 :이 UB입니다.


우선 C 언어에는 경계 검사가 없습니다. 사실상 거의 모든 것에 대해 전혀 확인하지 않습니다. 이것이 C의 기쁨이자 운명입니다.

이제 문제로 돌아가서 메모리 오버플로가 segfault를 트리거한다는 의미는 아닙니다. 어떻게 작동하는지 자세히 살펴 보겠습니다.

프로그램을 시작하거나 서브 루틴을 입력하면 프로세서는 함수가 끝날 때 반환되는 주소를 스택에 저장합니다.

스택은 프로세스 메모리 할당 중에 OS에서 초기화되었으며 리턴 주소를 저장할뿐만 아니라 원하는대로 읽고 쓸 수있는 합법적 인 메모리 범위를 확보했습니다.

컴파일러가 로컬 (자동) 변수를 만드는 데 사용하는 일반적인 방법은 스택에서 일부 공간을 예약하고 해당 공간을 변수에 사용하는 것입니다. 모든 함수 입력에서 찾을 수있는 잘 알려진 32 비트 어셈블러 시퀀스 인 prologue를 따라보십시오.

push ebp      ;save register on the stack
mov ebp,esp   ;get actual stack address
sub esp,4     ;displace the stack of 4 bytes that will be used to store a 4 chars array

스택이 데이터의 역방향으로 증가한다는 점을 고려할 때 메모리 레이아웃은 다음과 같습니다.

0x0.....1C   [Parameters (if any)]    ;former function
0x0.....18   [Return Address]
0x0.....14   EBP
0x0.....10   0x0......x               ;Local DWORD parameter
0x0.....0C   [Parameters (if any)]    ;our function
0x0.....08   [Return Address]
0x0.....04   EBP
0x0.....00   0, 'c', 'b', 'a'    ;our string of 3 chars plus final nul

이를 스택 프레임이라고합니다.

이제 0x0 .... 0에서 시작하여 0x .... 3에서 끝나는 4 바이트 문자열을 고려하십시오. 배열에 3 개 이상의 문자를 쓰면 저장된 EBP 사본, 반환 주소, 매개 변수, 이전 함수의 지역 변수, EBP, 반환 주소 등 순차적으로 교체합니다.

우리가 얻는 가장 시노 그래픽 효과는 함수 반환시 CPU가 segfault를 생성하는 잘못된 주소로 다시 점프하려고한다는 것 입니다. 지역 변수 중 하나가 포인터 인 경우에도 동일한 동작을 수행 할 수 있습니다.이 경우 잘못된 위치에 읽기 또는 쓰기를 시도하여 segfault를 다시 트리거합니다.

segfault가 발생할 수없는 경우 : 팽창 된 변수가 스택에 없거나 너무 많은 로컬 변수가있어 반환 주소를 건드리지 않고 덮어 쓸 때 (그리고 포인터가 아닙니다). 또 다른 경우는 프로세서가 로컬 변수와 반환 주소 사이에 보호 공간을 예약한다는 것입니다.이 경우 버퍼 오버플로가 주소에 도달하지 않습니다. 또 다른 가능성은 배열 요소에 무작위로 액세스하는 것입니다.이 경우 크기가 큰 배열은 스택 공간을 초과하고 다른 데이터에서 오버플로 될 수 있지만 다행히 반환 주소가 저장된 위치에 매핑 된 요소는 건드리지 않습니다 (모든 항목이 발생할 수 있음). .

스택에없는 segfault 팽창 변수를 언제 가질 수 있습니까? 배열 바운드 또는 포인터가 넘칠 때.

유용한 정보가 되셨기를 바랍니다.

참고 URL : https://stackoverflow.com/questions/26426910/is-accessing-a-global-array-outside-its-bound-undefined-behavior

반응형