저는 최근에 C를 배우기 시작했고 C를 주제로 수업을 들었습니다. 나는 현재 루프로 놀고 있고 설명하는 방법을 모르는 이상한 행동을하고 있습니다.
#include <stdio.h>
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%d \n", sizeof(array)/sizeof(int));
return 0;
}
Ubuntu 14.04를 실행하는 랩톱에서이 코드는 깨지지 않습니다. 완료됩니다. CentOS 6.6을 실행하는 학교 컴퓨터에서도 정상적으로 작동합니다. Windows 8.1에서 루프는 종료되지 않습니다.
더 이상한 점은 for
루프 조건을 다음과 같이 편집 i <= 11
하면 코드가 Ubuntu를 실행하는 랩톱에서만 종료 된다는 것 입니다. CentOS 및 Windows에서는 종료되지 않습니다.
누구든지 메모리에서 무슨 일이 일어나고 있으며 동일한 코드를 실행하는 다른 OS가 다른 결과를 낼 수 있는지 설명 할 수 있습니까?
편집 : for 루프가 범위를 벗어난 것을 알고 있습니다. 의도적으로하고 있습니다. OS와 컴퓨터마다 동작이 어떻게 다른지 알 수 없습니다.
답변
Ubuntu 14.04를 실행하는 랩톱 에서이 코드는 실행을 중단하지 않습니다. CentOS 6.6을 실행하는 학교 컴퓨터에서도 정상적으로 작동합니다. Windows 8.1에서 루프는 종료되지 않습니다.
더 이상한 점은
for
루프 조건을 다음과 같이 편집하면i <= 11
Ubuntu를 실행하는 랩톱에서만 코드가 종료 된다는 것 입니다. CentOS와 Windows는 종료되지 않습니다.
방금 메모리 스톰 핑을 발견했습니다. 자세한 내용은 여기를 참조하십시오 : “메모리 스톰프”란 무엇입니까?
당신이 할당 할 때 int array[10],i;
, 그 변수들은 메모리로 들어갑니다 (특히, 그것들은 함수와 관련된 메모리 블록 인 스택에 할당됩니다). array[]
그리고 i
메모리에서 서로 아마 인접. 윈도우 8.1에 보인다 i
에 위치해 있습니다 array[10]
. CentOS에서는 i
에 있습니다 array[11]
. 그리고 우분투에서는 어느 곳에도 없습니다 (아마도 array[-1]
?에 있습니다).
이 디버깅 문을 코드에 추가하십시오. 반복 10 또는 11에서을 array[i]
가리 킵니다 i
.
#include <stdio.h>
int main()
{
int array[10],i;
printf ("array: %p, &i: %p\n", array, &i);
printf ("i is offset %d from array\n", &i - array);
for (i = 0; i <=11 ; i++)
{
printf ("%d: Writing 0 to address %p\n", i, &array[i]);
array[i]=0; /*code should never terminate*/
}
return 0;
}
답변
버그는 다음 코드들 사이에 있습니다.
int array[10],i;
for (i = 0; i <=10 ; i++)
array[i]=0;
array
10 개의 요소 만 있기 때문에 마지막 반복 array[10] = 0;
에서 버퍼 오버 플로우가 발생합니다. 버퍼 오버플로는 정의되지 않은 동작 입니다. 즉, 하드 드라이브를 포맷하거나 악마가 코에서 날아갈 수 있습니다.
모든 스택 변수가 서로 인접하여 배치되는 것이 일반적입니다. 쓰기 i
위치에 있으면 array[10]
UB가로 재설정 i
되어 0
종료되지 않은 루프가 발생합니다.
수정하려면 루프 조건을로 변경하십시오 i < 10
.
답변
루프의 마지막 실행은 무엇입니까? array[10]
에 쓰지만 배열에는 0에서 9까지 번호가 지정된 10 개의 요소 만 있습니다. C 언어 사양에서는 이것이 “정의되지 않은 동작”이라고 말합니다. 이것이 실제로 의미하는 것은 프로그램이 int
메모리 바로 뒤에 array
있는 크기의 메모리 에 쓰려고 시도한다는 것 입니다. 그러면 실제로 발생하는 내용에 따라 달라지며 운영 체제뿐만 아니라 컴파일러, 컴파일러 옵션 (예 : 최적화 설정), 프로세서 아키텍처, 주변 코드에 따라 달라집니다. 예를 들어 주소 공간 무작위 화 (아마이 장난감 예제에는 없지만 실제 상황에서는 발생) 로 인해 실행에 따라 달라질 수도 있습니다 . 몇 가지 가능성은 다음과 같습니다.
- 위치가 사용되지 않았습니다. 루프가 정상적으로 종료됩니다.
- 위치는 값이 0 인 무언가에 사용되었습니다. 루프가 정상적으로 종료됩니다.
- 위치에 함수의 반환 주소가 포함되었습니다. 루프는 정상적으로 종료되지만 프로그램은 주소 0으로 점프하려고하기 때문에 충돌합니다.
- 위치는 변수를 포함합니다
i
.i
0에서 다시 시작 하기 때문에 루프가 종료되지 않습니다 . - 위치에 다른 변수가 있습니다. 루프는 정상적으로 종료되지만 “흥미로운”일이 발생합니다.
- 위치가 유효하지 않은 메모리 주소입니다. 예를 들어
array
가상 메모리 페이지의 끝에 있고 다음 페이지가 매핑되지 않기 때문입니다. - 악마는 코에서 날아갑니다 . 다행히도 대부분의 컴퓨터에는 필수 하드웨어가 없습니다.
Windows에서 관찰 한 것은 컴파일러가 변수 i
를 메모리 바로 다음에 메모리 에 배치하기로 결정 했기 때문에에 array[10] = 0
할당했습니다 i
. 우분투와 CentOS에서는 컴파일러가 설치되지 않았습니다 i
. 거의 모든 C 구현은 하나의 주요 예외를 제외 하고 메모리 에서 로컬 변수를 메모리 스택으로 그룹화합니다 . 일부 로컬 변수는 레지스터 에 완전히 배치 될 수 있습니다 . 변수가 스택에 있더라도 변수의 순서는 컴파일러에 의해 결정되며 소스 파일의 순서뿐만 아니라 유형에 따라 달라질 수 있습니다 (구멍을 남길 수있는 정렬 제약 조건에 메모리 낭비를 피하기 위해) , 컴파일러 이름, 컴파일러 내부 데이터 구조 등에 사용되는 일부 해시 값
컴파일러에서 수행하기로 결정한 작업을 찾으려면 어셈블러 코드를 보여 주도록 지시 할 수 있습니다. 아, 그리고 어셈블러를 해독하는 법을 배우십시오 (작성하는 것보다 쉽습니다). GCC (및 특히 유닉스 세계의 다른 컴파일러) -S
를 사용하면 바이너리 대신 어셈블러 코드를 생성 하는 옵션 을 전달하십시오 . 예를 들어, 다음은 최적화 옵션 -O0
(최적화 안 함)을 사용하여 amd64에서 GCC로 컴파일하는 루프에 대한 어셈블러 스 니펫입니다 (주석이 수동으로 추가됨).
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
여기서 변수 i
는 스택 상단에서 52 바이트 아래에있는 반면 배열은 스택 상단에서 48 바이트 아래에서 시작합니다. 따라서이 컴파일러 i
는 배열 바로 앞에 위치 합니다. 에 쓰면 덮어 쓰게 i
됩니다 array[-1]
. 로 변경 array[i]=0
하면 array[9-i]=0
이러한 특정 컴파일러 옵션을 사용하여이 특정 플랫폼에서 무한 루프를 얻게됩니다.
이제로 프로그램을 컴파일하자 gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
더 짧아요! 컴파일러는 스택 위치 할당을 거부했을 뿐 아니라 i
레지스터에 저장되어있을 ebx
뿐 아니라 array
요소를 설정하기 위한 메모리를 할당 하거나 요소를 설정하기위한 코드를 생성하지 않았습니다. 이제까지 사용되었습니다.
이 예제를보다 잘 설명하기 위해 컴파일러에서 최적화 할 수없는 것을 제공하여 배열 할당이 수행되도록합니다. 별도의 컴파일, 컴파일러가 (이 링크시 최적화 된 않는 다른 파일에 어떻게되는지 모르기 때문에 – 그 작업을 쉽게 수행하는 방법은 다른 파일에서 배열을 사용하는 것 gcc -O0
또는 gcc -O1
하지 않습니다). 다음을 포함하는 소스 파일 use_array.c
을 만듭니다.
void use_array(int *array) {}
소스 코드를
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
와 컴파일
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
이번에는 어셈블러 코드가 다음과 같습니다.
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
이제 배열은 스택에서 위에 44 바이트입니다. 무엇에 대해 i
? 어디에도 나타나지 않습니다! 그러나 루프 카운터는 레지스터에 유지됩니다 rbx
. 정확히는 i
아니지만의 주소입니다 array[i]
. 컴파일러는의 값이 i
직접 사용 된 적이 없기 때문에 루프를 실행할 때마다 0을 저장할 위치를 계산하기 위해 산술을 수행 할 필요가 없다고 결정했습니다 . 대신 해당 주소는 루프 변수이며, 경계를 결정하기위한 산술은 컴파일 타임 (배열 요소 당 11 반복에 4 바이트를 곱하여 44를 얻음)과 런타임에 한 번 또는 루프가 시작되기 전에 한 번만 수행되었습니다 ( 빼기를 수행하여 초기 값을 얻습니다).
이 매우 간단한 예에서도 컴파일러 옵션 변경 (최적화 켜기) 또는 사소한 변경 ( array[i]
~ array[9-i]
) 또는 관련이없는 것으로 변경 (호출 추가 use_array
)이 실행 프로그램이 생성하는 것과 큰 차이를 만드는 방법을 보았습니다. 컴파일러에 의해 수행됩니다. 컴파일러 최적화는 정의되지 않은 동작을 호출하는 프로그램에서 직관적이지 않은 것처럼 보일 수있는 많은 작업을 수행 할 수 있습니다 . 이것이 정의되지 않은 행동이 완전히 정의되지 않은 이유입니다. 실제 프로그램에서 트랙에서 약간 벗어나면 숙련 된 프로그래머조차도 코드의 기능과 수행해야 할 작업 간의 관계를 이해하기가 매우 어려울 수 있습니다.
답변
Java와 달리 C는 배열 경계 검사를 수행하지 않습니다. 즉, ArrayIndexOutOfBoundsException
배열 인덱스가 유효한지 확인하는 작업은 프로그래머에게 맡겨집니다. 이 작업을 의도적으로 수행하면 정의되지 않은 동작이 발생하여 어떤 일이 발생할 수 있습니다.
배열의 경우 :
int array[10]
인덱스는 0
~ 범위에서만 유효 합니다 9
. 그러나 다음을 시도하고 있습니다.
for (i = 0; i <=10 ; i++)
array[10]
여기에 액세스 하여 조건을i < 10
답변
경계 위반이 있으며 비 종료 플랫폼에서는 i
루프가 끝날 때 실수 로 0으로 설정 되어 다시 시작되도록 믿습니다 .
array[10]
유효하지 않다; 그것은 10 개 요소를 포함 array[0]
통해 array[9]
, 그리고 array[10]
11이다. 루프는 다음과 같이 전에 중지되도록 작성해야 10
합니다.
for (i = 0; i < 10; i++)
어디 array[10]
땅은 구현 정의하고, 재미있게, 당신의 플랫폼이에, 그것은에 토지 i
이러한 플랫폼은 분명히 바로 뒤에 배치하는 array
. i
는 0으로 설정되고 루프는 영원히 계속됩니다. 다른 플랫폼의 경우, i
이전 array
에 위치 하거나 array
그 뒤에 패딩이있을 수 있습니다.
답변
당신 은 인덱스 를 가지고 int array[10]
있음 array
을 선언 0
합니다 9
( 10
보유 할 수있는 총 정수 요소). 그러나 다음 루프는
for (i = 0; i <=10 ; i++)
루프 는 시간 0
을 10
의미 11
합니다. 따라서 i = 10
버퍼 오버플로가 발생하여 정의되지 않은 동작이 발생 합니다.
따라서 이것을 시도하십시오 :
for (i = 0; i < 10 ; i++)
또는,
for (i = 0; i <= 9 ; i++)
답변
에 정의되어 있지 array[10]
않으며 앞에서 설명한대로 정의되지 않은 동작 을 제공합니다 . 다음과 같이 생각하십시오.
식료품 카트에 10 개의 품목이 있습니다. 그들은:
0 : 시리얼
1 상자 : 빵
2 : 우유
3 : 파이
4 : 계란
5 : 케이크
6 : 2 리터의 소다
7 : 샐러드
8 : 햄버거
9 : 아이스크림
cart[10]
정의되지 않았으며 일부 컴파일러에서 범위를 벗어난 예외가 발생할 수 있습니다. 그러나 많은 것은 분명히 그렇지 않습니다. 명백한 11 번째 항목은 실제로 장바구니에 없는 항목입니다 . 열한 번째 항목은 내가 “폴더리스트 항목”이라고 부릅니다. 그것은 존재하지 않았지만 거기에있었습니다.
어떤 컴파일러가 제공하는 이유 i
의 인덱스를 array[10]
하거나 array[11]
또는 array[-1]
때문에 당신의 초기화 / 선언문이다. 일부 컴파일러는 이것을 다음과 같이 해석합니다.
- “10 개 블록 할당
int
에 대한들array[10]
과 다른int
. 블록 , 쉽게하기 위해 서로 바로 옆에 넣어.” - 이전과 동일하지만 공백을 한두 칸 이동하면을
array[10]
가리 키지 않습니다i
. - 이전과 동일한 작업을 수행하지만, 할당
i
에서array[-1]
, 또는 OS가 처리 할 수 있기 때문에 완전히 다른 지점에이를 할당하고 그것의 (배열의 인덱스는 할 수없는, 또는 음수가 될 수 없습니다해야하기 때문에) 안전합니다.
일부 컴파일러는 작업이 더 빨라지기를 원하고 일부 컴파일러는 안전을 선호합니다. 상황에 관한 모든 것입니다. 예를 들어 고대 BREW OS (기본 전화의 OS) 용 앱을 개발하는 경우 안전에 신경 쓰지 않습니다. iPhone 6을 개발 중이라면 무엇이든 빠르게 실행할 수 있으므로 안전에 중점을 둘 필요가 있습니다. (Apple의 App Store 지침을 읽거나 Swift 및 Swift 2.0 개발에 대해 읽어 보셨습니까?)