루프의 마지막 실행은 무엇입니까? 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
)이 실행 프로그램이 생성하는 것과 큰 차이를 만드는 방법을 보았습니다. 컴파일러에 의해 수행됩니다. 컴파일러 최적화는 정의되지 않은 동작을 호출하는 프로그램에서 직관적이지 않은 것처럼 보일 수있는 많은 작업을 수행 할 수 있습니다 . 이것이 정의되지 않은 행동이 완전히 정의되지 않은 이유입니다. 실제 프로그램에서 트랙에서 약간 벗어나면 숙련 된 프로그래머조차도 코드의 기능과 수행해야 할 작업 간의 관계를 이해하기가 매우 어려울 수 있습니다.