Xung đột giữa hướng dẫn Stanford và GCC


82

Theo bộ phim này (khoảng phút 38), nếu tôi có hai chức năng với các vars cục bộ giống nhau, chúng sẽ sử dụng cùng một không gian. Vì vậy, chương trình sau đây, nên in 5. Biên dịch nó với gcckết quả -1218960859. tại sao?

Chương trình:

#include <stdio.h>

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

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

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

như được yêu cầu, đây là đầu ra từ trình tháo gỡ:

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
"họ cũng sử dụng cùng một không gian" - điều đó không chính xác. Họ có thể. Hoặc họ có thể không. Và bạn cũng không thể dựa vào cách này.
Mat

17
Tôi tự hỏi cái này có công dụng gì như một bài tập, nếu người ta sử dụng cái này trong mã sản xuất thì người ta sẽ bị bắn.
AndersK

12
@claptrap Có thể để tìm hiểu cách hoạt động của ngăn xếp cuộc gọi và hiểu máy tính đang làm gì? Mọi người đang xem xét cách này quá nghiêm túc.
Jonathon Reinhart

9
@claptrap Một lần nữa, đó là một bài tập học tập . "Vòng lặp bạn phải nhảy qua" đều có ý nghĩa nếu bạn hiểu những gì đang diễn ra ở cấp độ lắp ráp. Tôi nghiêm túc nghi ngờ OP có bất kỳ ý định sử dụng một cái gì đó như thế này trong một chương trình "thực" (nếu anh ta, anh ta nên được đá!)
Jonathon Reinhart

12
Ví dụ này gây hiểu lầm cho người không nghi ngờ, bởi vì hai biến cục bộ có cùng tên; nhưng điều này không liên quan đến những gì đang diễn ra: Chỉ số lượng và loại biến mới quan trọng. Các tên khác nhau sẽ hoạt động hoàn toàn giống nhau.
alexis

Câu trả lời:


130

Vâng, vâng, đây là hành vi không xác định , vì bạn đang sử dụng biến chưa khởi tạo 1 .

Tuy nhiên, trên kiến ​​trúc x86 2 , thử nghiệm này sẽ hoạt động . Giá trị không bị "xóa" khỏi ngăn xếp và vì nó không được khởi tạo trong B()nên giá trị đó vẫn sẽ ở đó, miễn là các khung ngăn xếp giống hệt nhau.

Tôi muốn đoán rằng, vì int akhông được sử dụng bên trong void B(), trình biên dịch đã tối ưu hóa mã đó và số 5 không bao giờ được ghi vào vị trí đó trên ngăn xếp. Hãy thử thêm printfvào B()- nó chỉ có thể hoạt động.

Ngoài ra, cờ trình biên dịch - cụ thể là mức tối ưu hóa - cũng có thể sẽ ảnh hưởng đến thử nghiệm này. Thử tắt tối ưu hóa bằng cách chuyển -O0sang gcc.

Chỉnh sửa: Tôi vừa mới biên dịch mã của bạn với gcc -O0(64-bit) và thực sự, chương trình in ra 5, như một người quen thuộc với ngăn xếp cuộc gọi mong đợi. Trong thực tế, nó hoạt động ngay cả khi không có -O0. Bản dựng 32 bit có thể hoạt động khác.

Tuyên bố từ chối trách nhiệm: Đừng bao giờ, đừng bao giờ sử dụng những thứ như thế này trong mã "thực"!

1 - Có một cuộc tranh luận đang diễn ra bên dưới về việc liệu đây có phải là "UB" chính thức hay không, hay chỉ là không thể đoán trước.

2 - Ngoài ra x64 và có thể là mọi kiến ​​trúc khác sử dụng ngăn xếp cuộc gọi (ít nhất là những kiến ​​trúc có MMU)


Hãy xem một lý do tại sao nó không hoạt động. Điều này được nhìn thấy tốt nhất trong 32 bit, vì vậy tôi sẽ biên dịch với -m32.

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

Tôi đã biên dịch với $ gcc -m32 -O0 test.c(Tối ưu hóa bị vô hiệu hóa). Khi tôi chạy cái này, nó in ra rác.

Nhìn vào $ 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    

Chúng ta thấy rằng trong B, trình biên dịch dành riêng 0x10 byte không gian ngăn xếp và khởi tạo int abiến của chúng ta ở mức [ebp-0x4]5.

Trong Atuy nhiên, trình biên dịch đặt int atại [ebp-0xc]. Vì vậy, trong trường hợp này, các biến cục bộ của chúng ta không kết thúc ở cùng một vị trí! Bằng cách thêm một printf()cuộc gọi trong Acũng sẽ gây ra các khung stack cho ABđể được giống hệt nhau, và in 55.


7
Tuyên bố từ chối trách nhiệm tốt!
Tobias Wärre

5
Ngay cả khi nó hoạt động một lần, nó sẽ không đáng tin cậy trên một số kiến ​​trúc - một lời mở đầu gián đoạn của tôi sẽ thổi bay mọi thứ bên dưới con trỏ ngăn xếp bất cứ lúc nào.
Martin James

6
Rất nhiều phiếu bầu cho một câu trả lời thậm chí không đề cập đến "hành vi không xác định". Trên hết, nó cũng được chấp nhận.
BЈовић

25
Ngoài ra, nó được chấp nhận vì nó thực sự trả lời câu hỏi .
slbetman

8
@ BЈовић Bạn có xem video nào không? Hãy nhìn xem, mọi người và anh trai của họ đều biết rằng bạn không nên làm điều này trong mã thực, và nó tạo ra hành vi không xác định . Đó không phải là vấn đề. Vấn đề là máy tính là một cỗ máy được xác định rõ ràng, có thể dự đoán được. Trên hộp x86 (và có lẽ là hầu hết các kiến ​​trúc khác), với trình biên dịch lành mạnh và có khả năng tạo một số mã / cờ, điều này sẽ hoạt động như mong đợi. Đoạn mã này, cùng với video chỉ là một minh chứng về cách hoạt động của ngăn xếp cuộc gọi. Nếu nó làm phiền bạn đến mức đó, tôi khuyên bạn nên đi nơi khác. Một số người trong chúng ta thuộc loại tò mò thích tìm hiểu mọi thứ.
Jonathon Reinhart

36

Đó là hành vi không xác định . Một biến cục bộ chưa được khởi tạo có giá trị không xác định và việc sử dụng nó sẽ dẫn đến hành vi không xác định.


6
Nói chính xác hơn, việc sử dụng một biến đơn nguyên mà địa chỉ không bao giờ được sử dụng là hành vi không xác định.
Jens Gustedt

@JensGustedt Nhận xét tốt. Bạn có điều gì muốn nói về phần “Ví dụ tiếp theo” của blog.frama-c.com/index.php?post/2013/03/13/… không?
Pascal Cuoq,

@ PascalCuoq, đây thậm chí có vẻ là một cuộc thảo luận đang diễn ra trong ủy ban tiêu chuẩn. Có những tình huống mà việc kiểm tra bộ nhớ mà bạn nhận được thông qua một con trỏ có ý nghĩa, ngay cả khi bạn không thể biết liệu nó có được khởi tạo hay không. Chỉ đơn giản là làm cho nó không được xác định trong mọi trường hợp là quá hạn chế.
Jens Gustedt

@JensGustedt: Việc lấy địa chỉ của nó khiến cho việc sử dụng nó có hành vi được xác định như thế nào: { int uninit; &uninit; printf("%d\n", uninit); }vẫn có hành vi không xác định. Mặt khác, bạn có thể coi bất kỳ đối tượng nào là một mảng của unsigned char; đó có phải là những gì bạn đã nghĩ?
Keith Thompson.

@KeithThompson, không, ngược lại. Có một biến sao cho địa chỉ của nó không bao giờ được lấy nó không được khởi tạo sẽ dẫn đến UB. Bản thân việc đọc một giá trị không xác định không phải là hành vi không xác định, nội dung chỉ là không thể đoán trước. Từ 6.3.2.1 p2: Nếu lvalue chỉ định một đối tượng có thời lượng lưu trữ tự động mà có thể đã được khai báo với lớp lưu trữ thanh ghi (chưa bao giờ được sử dụng địa chỉ của nó) và đối tượng đó chưa được khởi tạo (không được khai báo với bộ khởi tạo và không được gán cho nó đã được thực hiện trước khi sử dụng), hành vi là không xác định.
Jens Gustedt

12

Một điều quan trọng cần nhớ - đừng bao giờ dựa vào những thứ như thế và đừng bao giờ sử dụng điều này trong mã thực! Đó chỉ là một điều thú vị (thậm chí không phải lúc nào cũng đúng), không phải là một tính năng hay một thứ gì đó tương tự. Hãy tưởng tượng bạn đang cố gắng tìm ra lỗi được tạo ra bởi loại "tính năng" đó - cơn ác mộng.

Btw. - C và C ++ có đầy đủ loại "tính năng" đó, đây là trình chiếu TUYỆT VỜI về nó: http://www.slideshare.net/olvemaudal/deep-c Vì vậy, nếu bạn muốn xem thêm các "tính năng" tương tự, hãy hiểu những gì dưới mui xe và cách nó hoạt động chỉ cần xem trình chiếu này - bạn sẽ không hối tiếc và tôi chắc chắn rằng ngay cả hầu hết các lập trình viên c / c ++ có kinh nghiệm cũng có thể học được nhiều điều từ điều này.


7

Trong hàm A, biến akhông được khởi tạo, việc in giá trị của nó dẫn đến hành vi không xác định.

Trong một số trình biên dịch, biến ain Aain Bở cùng một địa chỉ, vì vậy nó có thể in ra 5, nhưng một lần nữa, bạn không thể dựa vào hành vi không xác định.


1
Hướng dẫn này đúng 100%, nhưng liệu kết quả trên áp phích gốc s machine will be the same depends on the assembly generated by the compiler. As @JonathonReinhart pointed out the call to B () `có thể đã được tối ưu hóa hay không.
Lloyd Crawley

1
Tôi có một vấn đề với nội dung "hướng dẫn đó là sai". Bạn đã thực sự đi xem hướng dẫn? Nó không cố gắng dạy bạn cách làm điều điên rồ như thế này, mà là để chứng minh cách hoạt động của ngăn xếp cuộc gọi. Trong trường hợp đó, hướng dẫn là hoàn toàn chính xác.
Jonathon Reinhart

@JonathonReinhart Tôi không xem hướng dẫn, nghĩ rằng ví dụ này là từ hướng dẫn, tôi sẽ loại bỏ phần này.
Yu Hao

@LloydCrawley Tôi đã xóa phần về hướng dẫn. Tôi biết đó là về kiến ​​trúc ngăn xếp, ý tôi là chúng ở cùng một địa chỉ khi nó được in 5, nhưng rõ ràng Jonathon Reinhart có cách giải thích tốt hơn nhiều.
Yu Hao

7

Biên dịch mã của bạn với gcc -Wall filename.cBạn sẽ thấy những cảnh báo này.

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]  

Trong c In biến chưa được khởi tạo Dẫn đến hành vi Không xác định.

Phần 6.7.8 Khởi tạo tiêu chuẩn C99 cho biết

Nếu một đối tượng có thời lượng lưu trữ tự động không được khởi tạo rõ ràng, giá trị của nó là không xác định. Nếu một đối tượng có thời lượng lưu trữ tĩnh không được khởi tạo rõ ràng, thì:

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.

Chỉnh sửa1

Như @Jonathon Reinhart Nếu bạn tắt tối ưu hóa bằng cách Sử dụng -Ocờ gcc-O0 thì bạn có thể nhận được đầu ra 5.

Nhưng đây hoàn toàn không phải là ý kiến ​​hay, đừng bao giờ sử dụng điều này trong mã sản xuất.

-Wuninitialized Đây là một trong những cảnh báo có giá trị. Bạn nên cân nhắc cảnh báo này Bạn không nên vô hiệu hóa hoặc bỏ qua cảnh báo này, dẫn đến thiệt hại lớn trong quá trình sản xuất như gây ra sự cố trong khi chạy trình duyệt.


Edit2

Các trang trình bày Deep C giải thích Tại sao kết quả là 5 / rác. Thêm thông tin này từ các trang trình bày đó với các sửa đổi nhỏ để làm cho câu trả lời này hiệu quả hơn một chút.

Trường hợp 1: không có tối ưu hóa

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

Có lẽ trình biên dịch này có một nhóm các biến được đặt tên mà nó sử dụng lại. Ví dụ: biến a đã được sử dụng và phát hành B(), sau đó khi A()cần một số nguyên tên anó sẽ nhận được biến sẽ nhận được cùng một vị trí bộ nhớ. Nếu bạn đổi tên biến B()thành, giả sử b, thì tôi không nghĩ bạn sẽ nhận được 5.

Trường hợp 2: có tối ưu hóa

Rất nhiều điều có thể xảy ra khi trình tối ưu hóa hoạt động. Trong trường hợp này, tôi đoán rằng lệnh gọi đến B()có thể bị bỏ qua vì nó không có bất kỳ tác dụng phụ nào. Ngoài ra, tôi sẽ không ngạc nhiên nếu hàm A()được gạch trong main(), tức là không có lệnh gọi hàm. (Nhưng vì A ()có khả năng hiển thị trình liên kết, mã đối tượng cho hàm vẫn phải được tạo trong trường hợp tệp đối tượng khác muốn liên kết với hàm). Dù sao, tôi nghi ngờ giá trị được in sẽ là một cái gì đó khác nếu bạn tối ưu hóa mã.

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

Rác rưởi!


1
Logic của bạn trong Chỉnh sửa 2, Trường hợp 1 là hoàn toàn không chính xác. Đó không phải là ở tất cả các cách thức hoạt động. Tên của biến cục bộ hoàn toàn không có nghĩa.
Jonathon Reinhart

@JonathonReinhart Như đã đề cập trong câu trả lời, tôi đã thêm điều này từ các trang trình bày sâu, vui lòng giải thích dựa trên cơ sở nào mà nó không chính xác.
Gangadhar

3
Không có bất kỳ liên kết nào giữa không gian ngăn xếp và tên biến. Ví dụ dựa trên thực tế là về mặt khái niệm khung ngăn xếp trong lệnh gọi hàm thứ hai sẽ chỉ đơn giản là chồng lên khung ngăn xếp của lệnh gọi hàm thứ hai. Không quan trọng tên là gì, miễn là cả hai chữ ký phương thức đều giống nhau, điều tương tự có thể xảy ra. Như những người khác đã chỉ ra, nếu nó nằm trong một hệ thống nhúng và ngắt phần cứng được phục vụ giữa các lệnh gọi đến A () và B (), ngăn xếp sẽ chứa các giá trị ngẫu nhiên. Các công cụ cũ như Code Guard cho Borland cho phép viết các số 0 thành chồng trước mỗi cuộc gọi.
Dan Haynes

@DanHaynes Nhận xét của bạn thuyết phục tôi. Khung ngăn xếp trong lệnh gọi hàm thứ hai có thể chồng lên khung ngăn xếp của lệnh gọi hàm thứ nhất, vì kiểu biến và nguyên mẫu hàm giống nhau. Tôi cũng đồng ý vì không có gì liên quan đến tên biến.
Gangadhar
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.