Stanford 자습서와 GCC 간의 충돌


82

영화 (약 38 분) 에 따르면 동일한 로컬 변수를 가진 두 개의 함수가 있으면 동일한 공간을 사용합니다. 따라서 다음 프로그램은5 . gcc결과 와 함께 컴파일 -1218960859. 왜?

프로그램:

#include <stdio.h>

void A()
{
    int a;
    printf("%i",a);
}

void B()
{
    int a;
    a = 5;
}

int main()
{
    B();
    A();
    return 0;
}

요청한대로 디스어셈블러의 출력은 다음과 같습니다.

0804840c <A>:
 804840c:   55                      push   ebp
 804840d:   89 e5                   mov    ebp,esp
 804840f:   83 ec 28                sub    esp,0x28
 8048412:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048415:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048419:   c7 04 24 e8 84 04 08    mov    DWORD PTR [esp],0x80484e8
 8048420:   e8 cb fe ff ff          call   80482f0 <printf@plt>
 8048425:   c9                      leave  
 8048426:   c3                      ret    

08048427 <B>:
 8048427:   55                      push   ebp
 8048428:   89 e5                   mov    ebp,esp
 804842a:   83 ec 10                sub    esp,0x10
 804842d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048434:   c9                      leave  
 8048435:   c3                      ret    

08048436 <main>:
 8048436:   55                      push   ebp
 8048437:   89 e5                   mov    ebp,esp
 8048439:   83 e4 f0                and    esp,0xfffffff0
 804843c:   e8 e6 ff ff ff          call   8048427 <B>
 8048441:   e8 c6 ff ff ff          call   804840c <A>
 8048446:   b8 00 00 00 00          mov    eax,0x0
 804844b:   c9                      leave  
 804844c:   c3                      ret    
 804844d:   66 90                   xchg   ax,ax
 804844f:   90                      nop

41
"동일한 공간을 잘 사용합니다"-정답이 아닙니다. 그럴 수도 있습니다. 아니면 그렇지 않을 수도 있습니다. 그리고 당신은 어느 쪽이든 의지 할 수 없습니다.
Mat

17
나는 이것을 사용하는 것이 연습으로 무엇인지 궁금합니다.
AndersK

12
@claptrap 아마도 호출 스택이 어떻게 작동하는지 배우고 컴퓨터가 내부에서 무엇을하는지 이해하고 싶습니까? 사람들은 이런 식으로 너무 심각하게 받아들이고 있습니다.
조나단 라인 하트

9
@claptrap 다시 말하지만, 그것은 학습 연습 입니다. 조립 단계에서 무슨 일이 일어나고 있는지 이해한다면 "당신이 통과해야하는 농구"는 모두 의미가 있습니다. 나는 심각하게 영업 이익이 "진짜"프로그램 같은 것을 사용의 의도가 의심 (그가 않는 경우, 그가 쫓겨한다!)
조나단 라인 하트에게

12
두 지역 변수의 이름이 같기 때문에이 예제는 의심하지 않는 사람에게 오해의 소지가 있습니다. 그러나 이것은 무슨 일이 일어나고 있는지와는 무관합니다. 변수의 수와 유형 만 중요합니다. 다른 이름은 정확히 동일하게 작동해야합니다.
alexis

답변:


130

예, 예, 이것은 uninitialized 1 변수를 사용하고 있기 때문에 정의되지 않은 동작 입니다.

그러나 x86 아키텍처 2 에서는 이 실험이 작동 합니다. 값은 스택에서 "삭제"되지 않으며에서 초기화되지 않았기 때문에 B()스택 프레임이 동일하다면 동일한 값이 여전히 존재해야합니다.

나는 내부에서 사용int a 되지 않기 때문에 컴파일러가 해당 코드를 최적화했으며 5는 스택의 해당 위치에 기록되지 않았다고 생각합니다. 추가 시도 의를void B()printfB() 그냥 작동 할 수 있습니다 - 물론.

또한 컴파일러 플래그 (즉, 최적화 수준)도이 실험에 영향을 미칠 수 있습니다. -O0gcc 로 전달 하여 최적화를 비활성화하십시오 .

편집 : 방금 코드를 gcc -O0(64 비트)로 컴파일 했으며 실제로 프로그램은 호출 스택에 익숙한 사람이 예상하는 것처럼 5를 인쇄합니다. 실제로 -O0. 32 비트 빌드는 다르게 동작 할 수 있습니다.

면책 조항 : 음주하지 영원히, 영원히 "진짜"코드 같은 것을 사용!

1- 이것이 공식적으로 "UB"인지 아니면 단지 예측할 수 없는지 에 대한 논쟁이 계속되고 있습니다 .

2-또한 x64 및 호출 스택을 사용하는 다른 모든 아키텍처 (최소한 MMU가있는 아키텍처)


작동 하지 않는 이유를 살펴 보겠습니다 . 이것은 32 비트에서 가장 잘 볼 수 있으므로 -m32.

$ gcc --version
gcc (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)

$ gcc -m32 -O0 test.c(최적화 비활성화)로 컴파일했습니다 . 이것을 실행하면 쓰레기가 인쇄됩니다.

보고 $ objdump -Mintel -d ./a.out:

080483ec <A>:
 80483ec:   55                      push   ebp
 80483ed:   89 e5                   mov    ebp,esp
 80483ef:   83 ec 28                sub    esp,0x28
 80483f2:   8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 80483f5:   89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 80483f9:   c7 04 24 c4 84 04 08    mov    DWORD PTR [esp],0x80484c4
 8048400:   e8 cb fe ff ff          call   80482d0 <printf@plt>
 8048405:   c9                      leave  
 8048406:   c3                      ret    

08048407 <B>:
 8048407:   55                      push   ebp
 8048408:   89 e5                   mov    ebp,esp
 804840a:   83 ec 10                sub    esp,0x10
 804840d:   c7 45 fc 05 00 00 00    mov    DWORD PTR [ebp-0x4],0x5
 8048414:   c9                      leave  
 8048415:   c3                      ret    

우리는 점에서 볼 B, 컴파일러는 스택 공간을 0x10 바이트를 예약하고, 우리의 초기화 int a에 변수를 [ebp-0x4]5.

에서 A그러나, 컴파일러는 배치 int a에서 [ebp-0xc]. 따라서이 경우 우리의 지역 변수 같은 위치에 있지 않았습니다 ! 추가하는 것으로 써 printf()의 통화 A뿐만위한 스택 프레임 발생할하면서 AB동일하고, 인쇄 55.


7
좋은 면책 조항!
Tobias Wärre 2013 년

5
한 번 작동하더라도 일부 아키텍처에서는 신뢰할 수 없습니다. 인터럽트 프리앰블은 언제든지 스택 포인터 아래의 모든 것을 날려 버립니다.
Martin James

6
"정의되지 않은 동작"을 언급하지 않는 답변에 대한 많은 찬성표가 있습니다. 또한 허용됩니다.
BЈовић

25
또한 실제로 질문에 대답 하기 때문에 허용 됩니다.
slebetman

8
@ BЈовић 영상 보셨나요? 보세요, 모두와 그 형제는 실제 코드에서이 작업을 수행해서는 안된다는 것을 알고 있으며 정의되지 않은 동작을 호출합니다 . 그것은 요점이 아니다. 요점은 컴퓨터가 잘 정의되고 예측 가능한 기계라는 것입니다. 정상적인 컴파일러와 잠재적으로 일부 코드 / 플래그 마사지를 사용하는 x86 상자 (및 아마도 대부분의 다른 아키텍처)에서는 예상대로 작동 합니다. 비디오와 함께이 코드는 단순히 호출 스택이 어떻게 작동하는지 보여주는 데모 일뿐 입니다. 그렇게 심하게 괴롭다면 다른 곳으로 가십시오. 우리 중 일부는 사물을 이해하는 것을 좋아합니다.
Jonathon Reinhart

36

그건 정의되지 않은 동작 . 초기화되지 않은 지역 변수에는 불확실한 값이 있으며이를 사용하면 정의되지 않은 동작이 발생합니다.


6
더 정확하게 말하면 주소를 가져 오지 않는 단일화 변수를 사용하는 것은 정의되지 않은 동작입니다.
Jens Gustedt 2013 년

@JensGustedt 좋은 의견입니다. blog.frama-c.com/index.php?post/2013/03/13/…의 "다음 예제"섹션에 대해 할 말이 있습니까?
Pascal Cuoq 2013 년

@PascalCuoq, 이것은 표준위원회에서 진행중인 토론 인 것 같습니다. 포인터를 통해 얻은 메모리를 검사하는 것이 의미가있는 상황이 있습니다. 비록 초기화되었는지 여부를 알 수 없더라도 말입니다. 모든 경우에 정의되지 않은 것으로 만드는 것은 너무 제한적입니다.
Jens Gustedt 2013 년

@JensGustedt : 주소를 사용하면 어떻게 정의 된 동작을 갖게됩니다. { int uninit; &uninit; printf("%d\n", uninit); }여전히 정의되지 않은 동작이 있습니다. 반면에 모든 객체를 unsigned char; 그게 당신이 염두에 두었던 것입니까?
Keith Thompson

@KeithThompson, 아니 그 반대입니다. 해당 주소 찍은 적이되도록 변수를 갖는 하고 는 UB에 리드를 초기화되지 않았습니다. 불확실한 값을 읽는 것은 정의되지 않은 동작이 아니며 내용은 예측할 수 없습니다. 6.3.2.1 p2부터 : lvalue가 레지스터 저장소 클래스로 선언 될 수있는 자동 저장 기간의 개체를 지정하고 (그 주소를 사용하지 않은 경우) 해당 개체가 초기화되지 않은 경우 (이니셜 라이저로 선언되지 않고 할당되지 않은 경우) 사용하기 전에 수행 한 경우) 동작이 정의되지 않았습니다.
Jens Gustedt 2013 년

12

기억해야 할 한 가지 중요한 사항은 절대 그런 것에 의존 하지 말고 실제 코드에서 절대 사용 하지 마십시오 ! 그것은 단지 흥미로운 것입니다 (항상 사실은 아닙니다), 기능이나 그런 것이 아닙니다. 그런 종류의 "기능"-악몽에 의해 생성 된 버그를 찾으려고한다고 상상해보십시오.

Btw. -C와 C ++는 그런 종류의 "기능"으로 가득 차 있습니다. 여기에 대한 훌륭한 슬라이드 쇼가 있습니다. http://www.slideshare.net/olvemaudal/deep-c 따라서 더 유사한 "기능"을보고 싶다면 무엇이 있는지 이해하십시오. 이 슬라이드 쇼를 보시면 후회하지 않으실 것입니다. 경험 많은 C / C ++ 프로그래머들도 이것으로부터 많은 것을 배울 수있을 것입니다.


7

함수 A에서 변수 a는 초기화되지 않고 값을 인쇄하면 정의되지 않은 동작이 발생합니다.

일부 컴파일러에서는 ain Aain 변수 가 B동일한 주소에 있으므로 인쇄 5할 수 있지만 정의되지 않은 동작에 의존 할 수 없습니다.


1
튜토리얼은 100 % 맞지만, 원본 포스터 s machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to B ()` 의 결과 가 최적화 되었는지 여부 .
Lloyd Crawley 2013 년

1
"그 튜토리얼이 잘못되었습니다"라는 말에 문제가 있습니다. 실제로 튜토리얼을 보러 갔습니까? 이처럼 미친 짓을하는 방법을 가르치려는 것이 아니라 호출 스택이 어떻게 작동하는지 보여주기위한 것입니다. 이 경우 튜토리얼은 완전히 정확합니다.
조나단 라인 하트

@JonathonReinhart 튜토리얼을 보지 않았고이 예제가 튜토리얼에서 나온 것이라고 생각했습니다.이 부분을 제거하겠습니다.
Yu Hao

@LloydCrawley 튜토리얼에 대한 부분을 제거했습니다. 나는 그것이 스택 아키텍처에 관한 것임을 압니다. 그것이 인쇄했을 때 같은 주소에 있다는 것을 의미 5하지만 Jonathon Reinhart는 훨씬 더 나은 설명을 가지고 있습니다.
Yu Hao

7

코드를 컴파일 gcc -Wall filename.c하면 다음과 같은 경고가 표시됩니다.

In function 'B':
11:9: warning: variable 'a' set but not used [-Wunused-but-set-variable]

In function 'A':
6:11: warning: 'a' is used uninitialized in this function [-Wuninitialized]  

In c 초기화되지 않은 변수를 인쇄하면 정의되지 않은 동작이 발생합니다.

섹션 6.7.8 C99 표준 초기화에 따르면

자동 저장 기간이있는 개체가 명시 적으로 초기화되지 않은 경우 해당 값은 확정되지 않습니다. 정적 저장 기간이있는 객체가 명시 적으로 초기화되지 않은 경우 :

if it has pointer type, it is initialized to a null pointer;
— if it has arithmetic type, it is initialized to (positive or unsigned) zero;
— if it is an aggregate, every member is initialized (recursively) according to these rules;
— if it is a union, the first named member is initialized (recursively) according to these rules.

편집 1

@Jonathon Reinhart로 -O플래그 gcc-O0 를 사용하여 최적화를 비활성화 하면 출력 5를 얻을 수 있습니다.

그러나 이것은 전혀 좋은 생각이 아니며 프로덕션 코드에서 절대 사용하지 마십시오.

-Wuninitialized 이것은 귀중한 경고 중 하나입니다.이 경고를 고려해야합니다. 데몬을 실행하는 동안 충돌을 일으키는 것과 같이 프로덕션에서 막대한 손상을 초래하는이 경고를 비활성화하거나 건너 뛰지 마십시오.


편집 2

Deep C 슬라이드는 왜 결과가 5 / 가비지인지 설명했습니다.이 답변을 좀 더 효과적으로 만들기 위해 약간의 수정을 통해 해당 슬라이드에서이 정보를 추가합니다.

사례 1 : 최적화없이

$ gcc -O0 file.c && ./a.out  
5

아마도이 컴파일러에는 재사용하는 명명 된 변수 풀이 있습니다. 예를 들어 변수 a가에서 사용되고 해제 된 B()다음 A()정수 이름 a이 필요할 때 변수가 동일한 메모리 위치를 가져옵니다. 당신이 변수의 이름을 변경하는 경우 B()에, 말 b, 그때 당신이 얻을 것이라고 생각하지 않습니다 5.

사례 2 : 최적화

옵티마이 저가 시작될 때 많은 일이 발생할 수 있습니다.이 경우 B()부작용이 없기 때문에 호출을 건너 뛸 수 있다고 생각합니다 . 또한에 A()인라인 main(), 즉 함수 호출이없는 경우에는 놀라지 않을 것 입니다 . (그러나 A ()링커 가시성이 있으므로 다른 개체 파일이 함수와 연결하려는 경우에만 함수에 대한 개체 코드를 만들어야합니다.) 어쨌든 코드를 최적화하면 인쇄 된 값이 다른 것이 될 것이라고 생각합니다.

gcc -O file.c && ./a.out
1606415608  

찌꺼기!


1
편집 2, 사례 1의 논리가 완전히 잘못되었습니다. 그건 아닙니다 전혀 작동 방법. 지역 변수의 이름은 전혀 의미가 없습니다.
조나단 라인 하트

@JonathonReinhart 답변에서 언급했듯이 deepc 슬라이드에서 이것을 추가했습니다. 어떤 기준으로 잘못되었는지 설명하십시오.
Gangadhar 2013 년

3
스택 공간과 변수 이름 사이에는 연관성이 없습니다. 이 예제는 개념적으로 두 번째 함수 호출의 스택 프레임이 두 번째 함수 호출의 스택 프레임을 단순히 오버레이한다는 사실에 의존합니다. 이름이 무엇인지는 중요하지 않습니다. 두 메서드 시그니처가 같으면 같은 일이 발생할 수 있습니다. 다른 사람들이 지적했듯이 그것이 임베디드 시스템에 있고 하드웨어 인터럽트가 A ()와 B ()에 대한 호출 사이에 서비스 되었다면 스택에는 임의의 값이 포함될 것입니다. Code Guard for Borland와 같은 오래된 도구를 사용하면 각 호출 전에 스택에 0을 쓸 수있었습니다.
Dan Haynes 2013 년

@DanHaynes 귀하의 의견은 두 번째 함수 호출의 me.stack 프레임이 변수 유형 및 함수 프로토 타입이 동일한 한 First 함수 호출의 스택 프레임을 오버레이 할 수 있음을 확신시킵니다. 예, 변수 이름과 관련이 없다는데도 동의합니다.
Gangadhar 2013 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.