Một lỗi phân đoạn xảy ra khi một chương trình cố gắng truy cập bộ nhớ bên ngoài khu vực đã được phân bổ cho nó.
Trong trường hợp này, một lập trình viên C có kinh nghiệm có thể thấy rằng vấn đề đang xảy ra trong dòng sprintf
được gọi. Nhưng nếu bạn không thể biết lỗi xảy ra ở đâu hoặc nếu bạn không muốn đọc qua mã để cố gắng tìm ra nó, thì bạn có thể xây dựng chương trình của mình bằng các ký hiệu gỡ lỗi (với gcc
, -g
cờ thực hiện điều này ) và sau đó chạy nó thông qua một trình sửa lỗi.
Tôi đã sao chép mã nguồn của bạn và dán nó vào một tập tin mà tôi đặt tên slope.c
. Sau đó, tôi đã xây dựng nó như thế này:
gcc -Wall -g -o slope slope.c
(Đây -Wall
là tùy chọn. Nó chỉ để làm cho nó đưa ra cảnh báo cho nhiều tình huống hơn. Điều này có thể giúp tìm ra những gì có thể sai, quá.)
Sau đó, tôi đã chạy chương trình trong trình gỡ lỗi gdb
bằng cách chạy đầu tiên gdb ./slope
để bắt đầu gdb
với chương trình, và sau đó, một lần trong trình gỡ lỗi, đưa ra run
lệnh cho trình gỡ lỗi:
ek@Kip:~/source$ gdb ./slope
GNU gdb (GDB) 7.5-ubuntu
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/ek/source/slope...done.
(gdb) run
Starting program: /home/ek/source/slope
warning: Cannot call inferior functions, you have broken Linux kernel i386 NX (non-executable pages) support!
Program received signal SIGSEGV, Segmentation fault.
0x001a64cc in _IO_default_xsputn () from /lib/i386-linux-gnu/libc.so.6
(Đừng lo lắng về you have broken Linux kernel i386 NX
... support
tin nhắn của tôi ; nó không ngăn chặn gdb
việc sử dụng hiệu quả để gỡ lỗi chương trình này.)
Thông tin đó rất khó hiểu ... và nếu bạn không cài đặt các biểu tượng gỡ lỗi cho libc, thì bạn sẽ nhận được một tin nhắn thậm chí còn khó hiểu hơn có địa chỉ thập lục phân thay vì tên hàm biểu tượng _IO_default_xsputn
. May mắn thay, điều đó không thành vấn đề, bởi vì điều chúng tôi thực sự muốn biết là vấn đề đang xảy ra ở đâu trong chương trình của bạn .
Vì vậy, giải pháp là nhìn về phía sau, để xem những cuộc gọi chức năng nào đã diễn ra dẫn đến cuộc gọi chức năng cụ thể đó trong thư viện hệ thống nơi SIGSEGV
tín hiệu cuối cùng được kích hoạt.
gdb
(và bất kỳ trình gỡ lỗi nào) đều có tính năng này được tích hợp: nó được gọi là dấu vết ngăn xếp hoặc backtrace . Tôi sử dụng bt
lệnh gỡ lỗi để tạo backtrace trong gdb
:
(gdb) bt
#0 0x001a64cc in _IO_default_xsputn () from /lib/i386-linux-gnu/libc.so.6
#1 0x00178e04 in vfprintf () from /lib/i386-linux-gnu/libc.so.6
#2 0x0019b234 in vsprintf () from /lib/i386-linux-gnu/libc.so.6
#3 0x0017ff7b in sprintf () from /lib/i386-linux-gnu/libc.so.6
#4 0x080484cc in calc_slope (input1=1, input2=150) at slope.c:26
#5 0x08048578 in main () at slope.c:52
(gdb)
Bạn có thể thấy rằng main
hàm của bạn gọi calc_slope
hàm (mà bạn dự định), và sau đó calc_slope
gọi sprintf
, (trên hệ thống này) được triển khai với các cuộc gọi đến một vài chức năng thư viện liên quan khác.
Điều bạn thường quan tâm là chức năng gọi trong chương trình của bạn gọi một chức năng bên ngoài chương trình của bạn . Trừ khi có lỗi trong chính thư viện / thư viện mà bạn đang sử dụng (trong trường hợp này là thư viện C tiêu chuẩn libc
được cung cấp bởi tệp thư viện libc.so.6
), lỗi gây ra sự cố nằm trong chương trình của bạn và thường sẽ ở hoặc gần cuộc gọi cuối cùng trong chương trình của bạn.
Trong trường hợp này, đó là:
#4 0x080484cc in calc_slope (input1=1, input2=150) at slope.c:26
Đó là nơi chương trình của bạn gọi sprintf
. Chúng tôi biết điều này bởi vì sprintf
là bước tiếp theo lên. Nhưng ngay cả khi không nói rõ điều đó, bạn vẫn biết điều này bởi vì đó là những gì xảy ra trên dòng 26 và nó nói:
... at slope.c:26
Trong chương trình của bạn, dòng 26 chứa:
sprintf(s,"%d",curr);
.
Như đã thảo luận trong câu trả lời của Dennis Kaarsemaker , s
là một mảng một byte. (Không phải bằng 0, bởi vì giá trị bạn đã gán cho nó, ""
dài một byte, nghĩa là, nó bằng { '\0' }
, theo cùng một cách "Hello, world!\n"
tương đương với { 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\n', '\0' }
.)
Vậy, tại sao điều này vẫn có thể hoạt động trên một số nền tảng (và rõ ràng là khi được biên dịch với VC9 cho Windows)?
Mọi người thường nói rằng khi bạn phân bổ bộ nhớ và sau đó cố gắng truy cập bộ nhớ bên ngoài nó, điều đó sẽ gây ra lỗi. Nhưng điều đó không thực sự đúng. Theo tiêu chuẩn kỹ thuật của C và C ++, những gì nó thực sự tạo ra là hành vi không xác định.
Nói cách khác, bất cứ điều gì cũng có thể xảy ra!
Tuy nhiên, một số thứ có nhiều khả năng hơn những thứ khác. Tại sao một mảng nhỏ trên ngăn xếp sẽ, trên một số triển khai, dường như hoạt động như một mảng lớn hơn trên ngăn xếp?
Điều này dẫn đến cách phân bổ ngăn xếp được thực hiện, được phép thay đổi từ nền tảng này sang nền tảng khác. Tập tin thực thi của bạn có thể phân bổ nhiều bộ nhớ hơn cho ngăn xếp của nó so với thực tế dự định sẽ được sử dụng bất cứ lúc nào. Đôi khi, điều này có thể cho phép bạn ghi vào các vị trí bộ nhớ mà bạn chưa đặt yêu cầu rõ ràng trong mã của mình. Rất có khả năng đây là những gì đang xảy ra khi bạn xây dựng chương trình của mình trong VC9.
Tuy nhiên, bạn không nên dựa vào hành vi này ngay cả trong VC9. Nó có khả năng phụ thuộc vào các phiên bản thư viện khác nhau có thể tồn tại trên các hệ thống Windows khác nhau. Nhưng thậm chí nhiều khả năng là vấn đề không gian ngăn xếp thêm được phân bổ với ý định rằng nó sẽ thực sự được sử dụng, và vì vậy nó thực sự có thể được sử dụng.Sau đó, bạn trải qua cơn ác mộng đầy đủ của "hành vi không xác định", trong trường hợp này, nhiều hơn một biến có thể được lưu trữ ở cùng một nơi, nơi viết cho một ghi đè lên ... nhưng không phải lúc nào cũng vậy, bởi vì đôi khi ghi vào các biến được lưu trữ trong các thanh ghi và không thực sự được thực hiện ngay lập tức (hoặc đọc các biến có thể được lưu trong bộ nhớ cache hoặc một biến có thể được coi là giống như trước đây vì bộ nhớ được cấp cho nó bởi trình biên dịch không được ghi vào thông qua chính biến).
Và điều đó đưa tôi đến khả năng khác về lý do tại sao chương trình hoạt động khi được xây dựng với VC9. Có thể, và có khả năng, một số mảng hoặc biến khác thực sự được phân bổ bởi chương trình của bạn (có thể bao gồm việc được phân bổ bởi thư viện mà chương trình của bạn đang sử dụng) để sử dụng khoảng trắng sau mảng một byte s
. Vì vậy, việc xử lý s
như một mảng dài hơn một byte sẽ có tác dụng truy cập nội dung của các biến / mảng đó, cũng có thể là xấu.
Tóm lại, khi bạn gặp một lỗi như thế này, thật may mắn khi gặp một lỗi như "Lỗi phân đoạn" hoặc "Lỗi bảo vệ chung". Khi bạn không có điều đó, bạn có thể không phát hiện ra cho đến khi quá muộn rằng chương trình của bạn có hành vi không xác định.