Kiểm tra con trỏ về tính hợp lệ (C / C ++)


90

Có cách nào để xác định (tất nhiên là theo chương trình) nếu một con trỏ đã cho là "hợp lệ"? Kiểm tra NULL rất dễ dàng, nhưng những thứ như 0x00001234 thì sao? Khi cố gắng bỏ qua loại con trỏ này, một ngoại lệ / sự cố xảy ra.

Phương pháp đa nền tảng được ưu tiên, nhưng nền tảng cụ thể (cho Windows và Linux) cũng được.

Cập nhật để làm rõ: Vấn đề không phải là với con trỏ cũ / giải phóng / chưa được khởi tạo; thay vào đó, tôi đang triển khai một API lấy các con trỏ từ trình gọi (như một con trỏ đến một chuỗi, một trình xử lý tệp, v.v.). Người gọi có thể gửi (có mục đích hoặc do nhầm lẫn) một giá trị không hợp lệ dưới dạng con trỏ. Làm cách nào để ngăn chặn sự cố?



Tôi nghĩ rằng câu trả lời tích cực tốt nhất cho linux là do George Carrette đưa ra. Nếu điều đó vẫn chưa đủ, hãy xem xét việc xây dựng bảng ký hiệu chức năng vào thư viện, hoặc thậm chí một cấp độ bảng khác của các thư viện có sẵn với các bảng chức năng của riêng chúng. Sau đó, kiểm tra các bảng chính xác. Tất nhiên, những câu trả lời phủ định đó cũng đúng: bạn không thể thực sự chắc chắn 100% liệu con trỏ hàm có hợp lệ hay không trừ khi bạn đưa ra nhiều hạn chế bổ sung cho ứng dụng người dùng.
minghua

Đặc tả API có thực sự chỉ định nghĩa vụ như vậy để thực hiện không? Nhân tiện, tôi giả vờ không cho rằng bạn vừa là nhà phát triển vừa là nhà thiết kế. Quan điểm của tôi là, tôi không nghĩ rằng một API sẽ chỉ định một cái gì đó như "Trong trường hợp một con trỏ không hợp lệ được chuyển làm đối số, hàm phải xử lý sự cố và trả về NULL.". API thực hiện nghĩa vụ cung cấp dịch vụ trong các điều kiện sử dụng thích hợp, không phải bởi các vụ hack. Tuy nhiên, việc ngu ngốc một chút cũng chẳng có hại gì. Sử dụng tham chiếu làm cho những trường hợp như vậy ít lây lan hơn. :)
Poniros

Câu trả lời:


75

Cập nhật để làm rõ: Vấn đề không phải là con trỏ cũ, được giải phóng hoặc chưa được khởi tạo; thay vào đó, tôi đang triển khai một API lấy các con trỏ từ trình gọi (như một con trỏ đến một chuỗi, một trình xử lý tệp, v.v.). Người gọi có thể gửi (có mục đích hoặc do nhầm lẫn) một giá trị không hợp lệ dưới dạng con trỏ. Làm cách nào để ngăn chặn sự cố?

Bạn không thể thực hiện kiểm tra đó. Đơn giản là không có cách nào bạn có thể kiểm tra xem một con trỏ có "hợp lệ" hay không. Bạn phải tin tưởng rằng khi mọi người sử dụng một hàm lấy con trỏ, những người đó biết họ đang làm gì. Nếu họ chuyển cho bạn 0x4211 dưới dạng giá trị con trỏ, thì bạn phải tin tưởng nó trỏ đến địa chỉ 0x4211. Và nếu họ "vô tình" bắn trúng một đối tượng, thì ngay cả khi bạn sử dụng một số chức năng hệ thống hoạt động đáng sợ (IsValidPtr hoặc bất cứ điều gì), bạn vẫn sẽ gặp lỗi và không nhanh chóng thất bại.

Bắt đầu sử dụng con trỏ null để báo hiệu loại điều này và nói với người dùng thư viện của bạn rằng họ không nên sử dụng con trỏ nếu họ có xu hướng vô tình chuyển con trỏ không hợp lệ, nghiêm túc :)


Đây có lẽ là câu trả lời đúng nhưng tôi nghĩ một chức năng đơn giản kiểm tra các vị trí bộ nhớ hexspeak phổ biến sẽ hữu ích cho việc gỡ lỗi chung ... Ngay bây giờ tôi có một con trỏ đôi khi trỏ đến 0xfeeefeee và nếu tôi có một chức năng đơn giản mà tôi có thể sử dụng để tiêu các khẳng định xung quanh Nó sẽ giúp việc tìm ra thủ phạm dễ dàng hơn nhiều ... CHỈNH SỬA: Mặc dù tôi đoán sẽ không khó để tự viết ra một lời khẳng định ..
quant

@quant vấn đề là một số mã C và C ++ có thể thực hiện số học con trỏ trên một địa chỉ không hợp lệ mà không cần kiểm tra (dựa trên nguyên lý dọn rác, dọn rác) và do đó sẽ chuyển vào một con trỏ "được sửa đổi số học" từ một trong những địa chỉ đó -các địa chỉ không hợp lệ. Các trường hợp phổ biến đang tìm kiếm một phương thức từ một vtable không tồn tại dựa trên địa chỉ đối tượng không hợp lệ hoặc một trong các loại sai hoặc chỉ đơn giản là đọc các trường từ một con trỏ đến một cấu trúc không trỏ đến một.
rwong

Về cơ bản, điều này có nghĩa là bạn chỉ có thể lấy các chỉ số mảng từ thế giới bên ngoài. Một API phải tự bảo vệ khỏi người gọi không thể có con trỏ trong giao diện. Tuy nhiên, vẫn sẽ tốt nếu có macro để sử dụng trong các xác nhận về tính hợp lệ của con trỏ (mà bạn buộc phải có trong nội bộ). Nếu một con trỏ được đảm bảo trỏ vào bên trong một mảng có điểm bắt đầu và độ dài đã biết, thì điều đó có thể được kiểm tra một cách rõ ràng. Tốt hơn là bạn nên chết vì một vi phạm xác nhận (lỗi có tài liệu) hơn là một hành vi deref (lỗi không có tài liệu).
Rob

34

Dưới đây là ba cách dễ dàng để một chương trình C trong hệ điều hành Linux có được cái nhìn nội tâm về trạng thái của bộ nhớ mà nó đang chạy và tại sao câu hỏi lại có những câu trả lời phức tạp thích hợp trong một số ngữ cảnh.

  1. Sau khi gọi getpagesize () và làm tròn con trỏ đến ranh giới trang, bạn có thể gọi mincore () để tìm hiểu xem một trang có hợp lệ hay không và liệu nó có phải là một phần của quá trình làm việc không. Lưu ý rằng điều này yêu cầu một số tài nguyên hạt nhân, vì vậy bạn nên đánh giá nó và xác định xem việc gọi hàm này có thực sự thích hợp trong api của bạn hay không. Nếu api của bạn sẽ xử lý các ngắt hoặc đọc từ các cổng nối tiếp vào bộ nhớ, bạn nên gọi nó để tránh các hành vi không thể đoán trước.
  2. Sau khi gọi stat () để xác định xem có thư mục / proc / self khả dụng hay không, bạn có thể mở và đọc qua / proc / self / maps để tìm thông tin về khu vực mà con trỏ cư trú. Nghiên cứu trang man cho proc, hệ thống tệp giả thông tin quy trình. Rõ ràng là điều này tương đối tốn kém, nhưng bạn có thể thoát khỏi bộ nhớ đệm kết quả phân tích cú pháp vào một mảng mà bạn có thể tra cứu hiệu quả bằng cách sử dụng tìm kiếm nhị phân. Cũng xem xét / proc / self / smaps. Nếu api của bạn dành cho máy tính hiệu suất cao thì chương trình sẽ muốn biết về / proc / self / numa được ghi lại trong trang man cho numa, kiến ​​trúc bộ nhớ không đồng nhất.
  3. Lệnh gọi get_mempolicy (MPOL_F_ADDR) thích hợp cho công việc api tính toán hiệu suất cao trong đó có nhiều luồng thực thi và bạn đang quản lý công việc của mình để có mối quan hệ với bộ nhớ không đồng nhất vì nó liên quan đến lõi cpu và tài nguyên socket. Một api như vậy tất nhiên cũng sẽ cho bạn biết liệu một con trỏ có hợp lệ hay không.

Trong Microsoft Windows có hàm QueryWorkingSetEx được ghi lại trong API trạng thái quy trình (cũng trong API NUMA). Như một hệ quả tất yếu của lập trình API NUMA phức tạp, hàm này cũng sẽ cho phép bạn thực hiện công việc "kiểm tra con trỏ về tính hợp lệ (C / C ++)" đơn giản, vì vậy nó không có khả năng bị phản đối trong ít nhất 15 năm.


12
Câu trả lời đầu tiên không cố gắng trở nên đạo đức về bản thân câu hỏi và thực sự trả lời nó một cách hoàn hảo. Đôi khi mọi người không nhận ra rằng một người thực sự cần loại phương pháp gỡ lỗi này để tìm lỗi trong thư viện bên thứ 3 hoặc trong mã kế thừa vì ngay cả valgrind cũng chỉ tìm thấy các con trỏ đại diện khi thực sự truy cập chúng, không phải ví dụ: nếu bạn muốn thường xuyên kiểm tra các con trỏ xem có hợp lệ không trong một bảng bộ nhớ cache mà đã ghi đè từ một số nơi khác trong mã của bạn ...
lumpidu

Đây phải là câu trả lời được chấp nhận. Tôi đã làm điều đó tương tự trên nền tảng không phải Linux. Về cơ bản, nó hiển thị thông tin quy trình cho chính quy trình. Với khía cạnh này, có vẻ như windows hoạt động tốt hơn linux bằng cách hiển thị thông tin có ý nghĩa hơn thông qua API trạng thái quy trình.
minghua

31

Ngăn chặn sự cố do người gọi gửi đến một con trỏ không hợp lệ là một cách tốt để tạo ra các lỗi im lặng khó tìm.

Lập trình viên sử dụng API của bạn không phải là tốt hơn để nhận được thông báo rõ ràng rằng mã của anh ta là không có thật bằng cách làm hỏng nó thay vì ẩn nó?


8
Tuy nhiên, trong một số trường hợp, việc kiểm tra con trỏ xấu ngay lập tức khi API được gọi cách bạn thất bại sớm. Ví dụ: điều gì sẽ xảy ra nếu API lưu trữ con trỏ trong một cấu trúc dữ liệu, nơi nó sẽ chỉ được tham chiếu sau này? Sau đó, việc chuyển một con trỏ xấu cho API sẽ gây ra sự cố tại một số điểm ngẫu nhiên sau đó. Trong trường hợp đó, sẽ tốt hơn nếu thất bại sớm hơn, tại lệnh gọi API nơi giá trị xấu được đưa vào ban đầu.
peterflynn

28

Trên Win32 / 64 có một cách để thực hiện việc này. Cố gắng đọc con trỏ và bắt lỗi SEH kết quả sẽ được đưa ra khi bị lỗi. Nếu nó không ném, thì đó là một con trỏ hợp lệ.

Tuy nhiên, vấn đề với phương thức này là nó chỉ trả về việc bạn có thể đọc dữ liệu từ con trỏ hay không. Nó không đảm bảo về an toàn kiểu hoặc bất kỳ số lượng bất biến khác. Nói chung, phương pháp này tốt cho một số điều khác ngoài việc nói "vâng, tôi có thể đọc vị trí cụ thể đó trong bộ nhớ tại một thời điểm đã trôi qua".

Tóm lại, Đừng làm điều này;)

Raymond Chen có một bài đăng trên blog về chủ đề này: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx


3
@Tim, không có cách nào để làm điều đó trong C ++.
JaredPar

6
Đó chỉ là "câu trả lời đúng" nếu bạn xác định "con trỏ hợp lệ" là "không gây ra vi phạm quyền truy cập / segfault". Tôi muốn định nghĩa nó là "trỏ đến dữ liệu có ý nghĩa được phân bổ cho mục đích bạn sẽ sử dụng nó". Tôi cho rằng đó là một định nghĩa tốt hơn về tính hợp lệ của con trỏ ...;)
jalf

Ngay cả khi con trỏ hợp lệ cũng không thể được kiểm tra theo cách này. Hãy nghĩ đến thread1 () {.. if (IsValidPtr (p)) * p = 7; ...} thread2 () {sleep (1); xóa p; ...}
Christopher

2
@Christopher, rất đúng. Lẽ ra tôi phải nói "Tôi có thể đọc vị trí cụ thể đó trong bộ nhớ tại thời điểm hiện đã trôi qua"
JaredPar 15/02/09

@JaredPar: Đề xuất thực sự tồi. Có thể kích hoạt một trang bảo vệ, vì vậy ngăn xếp sẽ không được mở rộng sau này hoặc thứ gì đó đẹp đẽ tương tự.
Deduplicator

16

AFAIK không có cách nào. Bạn nên cố gắng tránh tình trạng này bằng cách luôn đặt con trỏ thành NULL sau khi giải phóng bộ nhớ.


4
Đặt một con trỏ thành null không cho bạn điều gì, ngoại trừ có thể là cảm giác an toàn sai.

Điều đó không đúng. Đặc biệt trong C ++ bạn có thể xác định xem có nên xóa các đối tượng thành viên hay không bằng cách kiểm tra giá trị null. Cũng lưu ý rằng, trong C ++, việc xóa con trỏ null là hợp lệ, do đó việc xóa vô điều kiện các đối tượng trong hàm hủy là phổ biến.
Ferdinand Beyer

4
int * p = new int (0); int * p2 = p; xóa p; p = NULL; xóa p2; // crash

1
zabzonk, và ?? những gì anh ấy nói là bạn có thể xóa một con trỏ null. p2 không phải là một con trỏ null, nhưng là một con trỏ không hợp lệ. bạn phải đặt nó thành null trước đó.
Johannes Schaub - litb 15/02/09

2
Nếu bạn có các bí danh cho bộ nhớ được trỏ tới, chỉ một trong số chúng sẽ được đặt thành NULL, các bí danh khác đang lủng lẳng xung quanh.
jdehaan


7

Về câu trả lời một chút trong chủ đề này:

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () dành cho Windows.

Lời khuyên của tôi là hãy tránh xa chúng, ai đó đã đăng bài này: http://blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

Một bài đăng khác về cùng chủ đề và của cùng một tác giả (tôi nghĩ) là bài này: http://blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx ("IsBadXxxPtr thực sự nên được gọi là CrashProgramRandomly ").

Nếu người dùng API của bạn gửi dữ liệu xấu, hãy để nó gặp sự cố. Nếu vấn đề là dữ liệu đã truyền không được sử dụng cho đến sau này (và điều đó khiến việc tìm ra nguyên nhân trở nên khó khăn hơn), hãy thêm chế độ gỡ lỗi trong đó các chuỗi, v.v. được ghi vào mục nhập. Nếu chúng tệ thì điều đó là hiển nhiên (và có thể là sụp đổ). Nếu nó xảy ra thường xuyên, có thể đáng để chuyển API của bạn ra khỏi quy trình và để chúng làm hỏng quy trình API thay vì quy trình chính.


Có lẽ một cách khác là sử dụng _CrtIsValidHeapPointer . Hàm này sẽ trả về TRUE nếu con trỏ hợp lệ và ném một ngoại lệ khi con trỏ được giải phóng. Theo tài liệu, chức năng này chỉ khả dụng trong CRT gỡ lỗi.
Crend King

6

Thứ nhất, tôi không thấy có ích gì khi cố gắng bảo vệ mình khỏi người gọi cố tình gây ra sự cố. Họ có thể dễ dàng làm điều này bằng cách cố gắng truy cập thông qua một con trỏ không hợp lệ. Có nhiều cách khác - chúng có thể chỉ ghi đè lên bộ nhớ của bạn hoặc ngăn xếp. Nếu bạn cần bảo vệ khỏi loại điều này thì bạn cần phải chạy trong một quy trình riêng bằng cách sử dụng socket hoặc một số IPC khác để giao tiếp.

Chúng tôi viết khá nhiều phần mềm cho phép đối tác / khách hàng / người dùng mở rộng chức năng. Chắc chắn bất kỳ lỗi nào đều được báo cáo cho chúng tôi trước tiên vì vậy rất hữu ích khi có thể dễ dàng chỉ ra rằng vấn đề nằm ở mã trình cắm. Ngoài ra, có những lo ngại về bảo mật và một số người dùng được tin cậy hơn những người khác.

Chúng tôi sử dụng một số phương pháp khác nhau tùy thuộc vào yêu cầu về hiệu suất / thông lượng và độ tin cậy. Từ ưu tiên nhất:

  • các quy trình riêng biệt bằng cách sử dụng các socket (thường truyền dữ liệu dưới dạng văn bản).

  • các quy trình riêng biệt bằng cách sử dụng bộ nhớ dùng chung (nếu số lượng lớn dữ liệu cần vượt qua).

  • cùng một quá trình phân tách các luồng thông qua hàng đợi tin nhắn (nếu các tin nhắn ngắn thường xuyên).

  • cùng một quy trình, các luồng riêng biệt tất cả dữ liệu được truyền từ một nhóm bộ nhớ.

  • cùng một quy trình thông qua cuộc gọi thủ tục trực tiếp - tất cả dữ liệu được chuyển từ một nhóm bộ nhớ.

Chúng tôi cố gắng không bao giờ sử dụng đến những gì bạn đang cố gắng làm khi xử lý phần mềm của bên thứ ba - đặc biệt là khi chúng tôi được cung cấp các trình cắm / thư viện dưới dạng nhị phân chứ không phải mã nguồn.

Sử dụng nhóm bộ nhớ khá dễ dàng trong hầu hết các trường hợp và không cần phải kém hiệu quả. Nếu BẠN phân bổ dữ liệu ngay từ đầu thì việc kiểm tra các con trỏ so với các giá trị mà bạn đã phân bổ là điều tầm thường. Bạn cũng có thể lưu trữ độ dài được phân bổ và thêm các giá trị "ma thuật" trước và sau dữ liệu để kiểm tra kiểu dữ liệu hợp lệ và các lần vượt dữ liệu.


4

Tôi rất thông cảm với câu hỏi của bạn, vì bản thân tôi cũng đang ở một vị trí gần như giống hệt nhau. Tôi đánh giá cao những gì rất nhiều câu trả lời đang nói và chúng đúng - quy trình cung cấp con trỏ phải cung cấp một con trỏ hợp lệ. Trong trường hợp của tôi, hầu như không thể tưởng tượng được rằng họ có thể đã làm hỏng con trỏ - nhưng nếu họ đã quản lý, phần mềm của TÔI sẽ bị lỗi và TÔI sẽ nhận lỗi :-(

Yêu cầu của tôi không phải là tôi tiếp tục sau khi xảy ra lỗi phân đoạn - điều đó sẽ nguy hiểm - tôi chỉ muốn báo cáo những gì đã xảy ra với khách hàng trước khi chấm dứt để họ có thể sửa mã của họ thay vì đổ lỗi cho tôi!

Đây là cách tôi đã tìm thấy để làm điều đó (trên Windows): http://www.cplusplus.com/reference/clibrary/csignal/signal/

Để đưa ra tóm tắt:

#include <signal.h>

using namespace std;

void terminate(int param)
/// Function executed if a segmentation fault is encountered during the cast to an instance.
{
  cerr << "\nThe function received a corrupted reference - please check the user-supplied  dll.\n";
  cerr << "Terminating program...\n";
  exit(1);
}

...
void MyFunction()
{
    void (*previous_sigsegv_function)(int);
    previous_sigsegv_function = signal(SIGSEGV, terminate);

    <-- insert risky stuff here -->

    signal(SIGSEGV, previous_sigsegv_function);
}

Bây giờ điều này dường như hoạt động như tôi hy vọng (nó in thông báo lỗi, sau đó chấm dứt chương trình) - nhưng nếu ai đó có thể phát hiện ra lỗ hổng, vui lòng cho tôi biết!


Không sử dụng exit(), nó làm hỏng RAII và do đó có thể gây rò rỉ tài nguyên.
Sebastian Mach

Thật thú vị - có một cách khác để kết thúc gọn gàng trong tình huống này không? Và câu lệnh thoát có phải là vấn đề duy nhất khi thực hiện nó như thế này không? Tôi nhận thấy rằng tôi đã đạt được "-1" - đó chỉ là vì 'lối ra'?
Mike Sadler

Rất tiếc, tôi nhận thấy đây là một tình huống khá đặc biệt. Tôi chỉ thấyexit() và Chuông báo động C ++ di động của tôi bắt đầu đổ chuông. Sẽ không sao trong tình huống cụ thể của Linux này, nơi chương trình của bạn sẽ thoát ra bất cứ lúc nào, xin lỗi vì tiếng ồn.
Sebastian Mach vào

1
signal (2) không di động. Sử dụng sigaction (2). man 2 signaltrên Linux có một đoạn giải thích tại sao.
rptb1

1
Trong tình huống này, tôi thường gọi là hủy bỏ (3) hơn là thoát (3) vì nó có nhiều khả năng tạo ra một số loại lỗi gỡ lỗi tồn đọng mà bạn có thể sử dụng để chẩn đoán vấn đề sau khi khám nghiệm. Trên hầu hết Unixen, hủy bỏ (3) sẽ kết xuất lõi (nếu cho phép kết xuất lõi) và trên Windows, nó sẽ cung cấp khởi chạy trình gỡ lỗi nếu được cài đặt.
rptb1

4

Trên Unix, bạn có thể sử dụng một cuộc gọi tổng hợp hạt nhân thực hiện kiểm tra con trỏ và trả về EFAULT, chẳng hạn như:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>

bool isPointerBad( void * p )
{
   int fh = open( p, 0, 0 );
   int e = errno;

   if ( -1 == fh && e == EFAULT )
   {
      printf( "bad pointer: %p\n", p );
      return true;
   }
   else if ( fh != -1 )
   {
      close( fh );
   }

   printf( "good pointer: %p\n", p );
   return false;
}

int main()
{
   int good = 4;
   isPointerBad( (void *)3 );
   isPointerBad( &good );
   isPointerBad( "/tmp/blah" );

   return 0;
}

trở lại:

bad pointer: 0x3
good pointer: 0x7fff375fd49c
good pointer: 0x400793

Có lẽ có một syscall tốt hơn để sử dụng so với open () [có lẽ là quyền truy cập], vì có khả năng điều này có thể dẫn đến codepath tạo tệp thực tế và yêu cầu đóng tiếp theo.


Đây là một hack tuyệt vời. Tôi rất muốn xem lời khuyên về các cuộc gọi tổng hợp khác nhau để xác nhận phạm vi bộ nhớ, đặc biệt nếu chúng có thể được đảm bảo không có tác dụng phụ. Bạn có thể mở bộ mô tả tệp để ghi vào / dev / null để kiểm tra xem bộ đệm có trong bộ nhớ có thể đọc được hay không, nhưng có lẽ có nhiều giải pháp đơn giản hơn. Tốt nhất tôi có thể tìm thấy là liên kết biểu tượng (ptr, "") sẽ đặt errno thành 14 trên địa chỉ xấu hoặc 2 trên địa chỉ tốt, nhưng các thay đổi hạt nhân có thể hoán đổi thứ tự xác minh.
Preston

@Preston Trong DB2, tôi nghĩ rằng chúng ta đã từng sử dụng quyền truy cập của unistd.h (). Tôi đã sử dụng open () ở trên vì nó ít tối nghĩa hơn một chút, nhưng bạn có thể đúng rằng có rất nhiều syscalls có thể sử dụng. Windows từng có API kiểm tra con trỏ rõ ràng, nhưng hóa ra nó không an toàn cho chuỗi (tôi nghĩ nó đã sử dụng SEH để cố gắng ghi và sau đó khôi phục giới hạn của phạm vi bộ nhớ.)
Peeter Joot

2

Không có điều khoản nào trong C ++ để kiểm tra tính hợp lệ của một con trỏ như một trường hợp chung. Rõ ràng người ta có thể cho rằng NULL (0x00000000) là không tốt và nhiều trình biên dịch và thư viện khác nhau thích sử dụng "giá trị đặc biệt" ở đây và ở đó để giúp gỡ lỗi dễ dàng hơn (Ví dụ: nếu tôi từng thấy một con trỏ hiển thị dưới dạng 0xCECECECE trong studio trực quan, tôi biết Tôi đã làm sai điều gì đó) nhưng sự thật là vì một con trỏ chỉ là một chỉ mục trong bộ nhớ nên gần như không thể biết chỉ bằng cách nhìn vào con trỏ nếu đó là chỉ mục "đúng".

Có nhiều thủ thuật khác nhau mà bạn có thể thực hiện với dynamic_cast và RTTI, chẳng hạn như để đảm bảo rằng đối tượng được trỏ đến thuộc loại mà bạn muốn, nhưng tất cả chúng đều yêu cầu bạn phải trỏ đến thứ hợp lệ ngay từ đầu.

Nếu bạn muốn đảm bảo rằng chương trình của bạn có thể phát hiện con trỏ "không hợp lệ" thì lời khuyên của tôi là: Đặt mọi con trỏ bạn khai báo thành NULL hoặc một địa chỉ hợp lệ ngay khi tạo và đặt nó thành NULL ngay sau khi giải phóng bộ nhớ mà nó trỏ đến. Nếu bạn siêng năng thực hành này, thì việc kiểm tra NULL là tất cả những gì bạn cần.


Hằng số con trỏ null trong C ++ (hoặc C, đối với vấn đề đó), được biểu diễn bằng một số không tích phân không đổi. Rất nhiều triển khai sử dụng tất cả các số không nhị phân để đại diện cho nó, nhưng nó không phải là thứ đáng tin cậy.
David Thornley

2

Không có bất kỳ cách di động nào để làm điều này và thực hiện nó cho các nền tảng cụ thể có thể ở bất kỳ đâu giữa khó và không thể. Trong mọi trường hợp, bạn không bao giờ nên viết mã phụ thuộc vào việc kiểm tra như vậy - đừng để các con trỏ nhận các giá trị không hợp lệ ngay từ đầu.


2

Đặt con trỏ thành NULL trước và sau khi sử dụng là một kỹ thuật tốt. Điều này rất dễ thực hiện trong C ++ nếu bạn quản lý các con trỏ trong một lớp chẳng hạn (một chuỗi):

class SomeClass
{
public:
    SomeClass();
    ~SomeClass();

    void SetText( const char *text);
    char *GetText() const { return MyText; }
    void Clear();

private:
    char * MyText;
};


SomeClass::SomeClass()
{
    MyText = NULL;
}


SomeClass::~SomeClass()
{
    Clear();
}

void SomeClass::Clear()
{
    if (MyText)
        free( MyText);

    MyText = NULL;
}



void SomeClass::Settext( const char *text)
{
    Clear();

    MyText = malloc( strlen(text));

    if (MyText)
        strcpy( MyText, text);
}

Câu hỏi cập nhật làm cho câu trả lời của tôi sai, tất nhiên (hoặc ít nhất là một câu trả lời cho một câu hỏi khác). Tôi đồng ý với các câu trả lời về cơ bản nói rằng, hãy để họ sụp đổ nếu họ lạm dụng thứ. Bạn không thể ngăn mọi người dùng búa đánh mình vào ngón tay cái ...
Tim Ring

2

Việc chấp nhận các con trỏ tùy ý làm tham số đầu vào trong một API công khai không phải là một chính sách tốt. Tốt hơn nên có các loại "dữ liệu thuần túy" như số nguyên, chuỗi hoặc cấu trúc (ý tôi là một cấu trúc cổ điển với dữ liệu thuần túy bên trong, tất nhiên; chính thức thì mọi thứ đều có thể là cấu trúc).

Tại sao? Vì như những người khác nói không có cách chuẩn nào để biết liệu bạn đã được cấp một con trỏ hợp lệ hay con trỏ trỏ tới rác.

Nhưng đôi khi bạn không có sự lựa chọn - API của bạn phải chấp nhận một con trỏ.

Trong những trường hợp này, nhiệm vụ của người gọi là phải vượt qua một con trỏ tốt. NULL có thể được chấp nhận là một giá trị, nhưng không phải là một con trỏ tới rác.

Bạn có thể kiểm tra lại bằng bất kỳ cách nào? Chà, những gì tôi đã làm trong trường hợp như vậy là xác định một bất biến cho kiểu con trỏ trỏ tới và gọi nó khi bạn nhận được nó (trong chế độ gỡ lỗi). Ít nhất nếu bất biến không thành công (hoặc bị treo), bạn biết rằng bạn đã được chuyển một giá trị xấu.

// API that does not allow NULL
void PublicApiFunction1(Person* in_person)
{
  assert(in_person != NULL);
  assert(in_person->Invariant());

  // Actual code...
}

// API that allows NULL
void PublicApiFunction2(Person* in_person)
{
  assert(in_person == NULL || in_person->Invariant());

  // Actual code (must keep in mind that in_person may be NULL)
}

re: "truyền một kiểu dữ liệu thuần túy ... như một chuỗi" Nhưng trong C ++, các chuỗi thường được truyền dưới dạng con trỏ đến ký tự, (char *) hoặc (const char *), vì vậy bạn quay lại chuyển con trỏ. Và ví dụ của bạn chuyển in_man như một tham chiếu, không phải là một con trỏ, vì vậy so sánh (in_man! = NULL) ngụ ý rằng có một số so sánh đối tượng / con trỏ được xác định trong lớp Person.
Jesse Chisholm

@JesseChisholm Theo chuỗi, tôi có nghĩa là một chuỗi, tức là một std :: string. Tôi khuyên bạn không nên sử dụng char * như một cách để lưu trữ các chuỗi hoặc chuyển chúng đi khắp nơi. Đừng làm vậy.
Daniel Daranas

@JesseChisholm Vì một số lý do, tôi đã mắc sai lầm khi trả lời câu hỏi này cách đây 5 năm. Rõ ràng, không có ý nghĩa gì khi kiểm tra xem một Người & có ĐẦY ĐỦ hay không. Điều đó thậm chí sẽ không biên dịch. Ý tôi là sử dụng con trỏ, không phải tham chiếu. Tôi đã sửa nó ngay bây giờ.
Daniel Daranas

1

Như những người khác đã nói, bạn không thể phát hiện một con trỏ không hợp lệ một cách đáng tin cậy. Hãy xem xét một số dạng mà con trỏ không hợp lệ có thể sử dụng:

Bạn có thể có một con trỏ null. Đó là một trong những bạn có thể dễ dàng kiểm tra và làm điều gì đó.

Bạn có thể có một con trỏ đến một nơi nào đó bên ngoài bộ nhớ hợp lệ. Điều gì tạo nên bộ nhớ hợp lệ khác nhau tùy thuộc vào cách môi trường thời gian chạy của hệ thống của bạn thiết lập không gian địa chỉ. Trên các hệ thống Unix, nó thường là một không gian địa chỉ ảo bắt đầu từ 0 và chuyển đến một số lượng lớn megabyte. Trên các hệ thống nhúng, nó có thể khá nhỏ. Nó có thể không bắt đầu từ 0, trong mọi trường hợp. Nếu ứng dụng của bạn đang chạy ở chế độ người giám sát hoặc chế độ tương đương, thì con trỏ của bạn có thể tham chiếu đến một địa chỉ thực, địa chỉ này có thể được sao lưu hoặc không được sao lưu bằng bộ nhớ thực.

Bạn có thể có một con trỏ đến một nơi nào đó bên trong bộ nhớ hợp lệ của mình, thậm chí bên trong phân đoạn dữ liệu, bss, ngăn xếp hoặc heap của bạn, nhưng không trỏ đến một đối tượng hợp lệ. Một biến thể của nó là một con trỏ được sử dụng để trỏ đến một đối tượng hợp lệ, trước khi có điều gì đó tồi tệ xảy ra với đối tượng. Những điều tồi tệ trong bối cảnh này bao gồm phân bổ giao dịch, hỏng bộ nhớ hoặc hỏng con trỏ.

Bạn có thể có một con trỏ bất hợp pháp phẳng, chẳng hạn như một con trỏ có căn chỉnh bất hợp pháp cho thứ đang được tham chiếu.

Vấn đề thậm chí còn trở nên tồi tệ hơn khi bạn xem xét các kiến ​​trúc dựa trên phân đoạn / bù đắp và các triển khai con trỏ kỳ lạ khác. Loại thứ này thường bị nhà phát triển che giấu bởi trình biên dịch tốt và sử dụng hợp lý các loại, nhưng nếu bạn muốn xuyên qua bức màn và cố gắng vượt qua các nhà phát triển hệ điều hành và trình biên dịch, bạn có thể, nhưng không có một cách chung để làm điều đó sẽ xử lý tất cả các vấn đề bạn có thể gặp phải.

Điều tốt nhất bạn có thể làm là cho phép sự cố và đưa ra một số thông tin chẩn đoán tốt.


re: "đưa ra một số thông tin chẩn đoán tốt", có một chà. Vì bạn không thể kiểm tra tính hợp lệ của con trỏ, thông tin bạn phải bận tâm là rất ít. "Một ngoại lệ đã xảy ra ở đây," có thể là tất cả những gì bạn nhận được. Toàn bộ ngăn xếp cuộc gọi là tốt, nhưng yêu cầu một khuôn khổ tốt hơn hầu hết các thư viện thời gian chạy C ++ cung cấp.
Jesse Chisholm


1

Nói chung là không thể làm được. Đây là một trường hợp đặc biệt khó chịu:

struct Point2d {
    int x;
    int y;
};

struct Point3d {
    int x;
    int y;
    int z;
};

void dump(Point3 *p)
{
    printf("[%d %d %d]\n", p->x, p->y, p->z);
}

Point2d points[2] = { {0, 1}, {2, 3} };
Point3d *p3 = reinterpret_cast<Point3d *>(&points[0]);
dump(p3);

Trên nhiều nền tảng, điều này sẽ in ra:

[0 1 2]

Bạn đang buộc hệ thống thời gian chạy giải thích sai các bit của bộ nhớ, nhưng trong trường hợp này, nó sẽ không bị lỗi, bởi vì các bit đều có ý nghĩa. Đây là một phần của thiết kế của ngôn ngữ (xem C-phong cách đa hình với struct inaddr, inaddr_in, inaddr_in6), do đó bạn có thể không đáng tin cậy bảo vệ chống lại nó trên nền tảng nào.


1

Thật không thể tin được là bạn có thể đọc được bao nhiêu thông tin sai lệch trong các bài báo trên ...

Và ngay cả trong tài liệu msdn của microsoft, IsBadPtr được cho là bị cấm. Ồ, tôi thích ứng dụng đang hoạt động hơn là bị treo. Ngay cả khi thời hạn làm việc có thể hoạt động không chính xác (miễn là người dùng cuối có thể tiếp tục với ứng dụng).

Bằng cách sử dụng googling, tôi không tìm thấy bất kỳ ví dụ hữu ích nào cho windows - đã tìm thấy giải pháp cho các ứng dụng 32 bit,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2Fsrc%2Fdtid295&obID = 2

nhưng tôi cũng cần hỗ trợ các ứng dụng 64-bit, vì vậy giải pháp này không phù hợp với tôi.

Nhưng tôi đã thu thập mã nguồn của rượu vang và quản lý để nấu loại mã tương tự cũng sẽ hoạt động cho các ứng dụng 64 bit - đính kèm mã ở đây:

#include <typeinfo.h>   

typedef void (*v_table_ptr)();   

typedef struct _cpp_object   
{   
    v_table_ptr*    vtable;   
} cpp_object;   



#ifndef _WIN64
typedef struct _rtti_object_locator
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    const type_info *type_descriptor;
    //const rtti_object_hierarchy *type_hierarchy;
} rtti_object_locator;
#else

typedef struct
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    unsigned int type_descriptor;
    unsigned int type_hierarchy;
    unsigned int object_locator;
} rtti_object_locator;  

#endif

/* Get type info from an object (internal) */  
static const rtti_object_locator* RTTI_GetObjectLocator(void* inptr)  
{   
    cpp_object* cppobj = (cpp_object*) inptr;  
    const rtti_object_locator* obj_locator = 0;   

    if (!IsBadReadPtr(cppobj, sizeof(void*)) &&   
        !IsBadReadPtr(cppobj->vtable - 1, sizeof(void*)) &&   
        !IsBadReadPtr((void*)cppobj->vtable[-1], sizeof(rtti_object_locator)))  
    {  
        obj_locator = (rtti_object_locator*) cppobj->vtable[-1];  
    }  

    return obj_locator;  
}  

Và mã sau có thể phát hiện xem con trỏ có hợp lệ hay không, bạn có thể cần thêm một số kiểm tra NULL:

    CTest* t = new CTest();
    //t = (CTest*) 0;
    //t = (CTest*) 0x12345678;

    const rtti_object_locator* ptr = RTTI_GetObjectLocator(t);  

#ifdef _WIN64
    char *base = ptr->signature == 0 ? (char*)RtlPcToFileHeader((void*)ptr, (void**)&base) : (char*)ptr - ptr->object_locator;
    const type_info *td = (const type_info*)(base + ptr->type_descriptor);
#else
    const type_info *td = ptr->type_descriptor;
#endif
    const char* n =td->name();

Điều này nhận được tên lớp từ con trỏ - Tôi nghĩ nó sẽ đủ cho nhu cầu của bạn.

Một điều mà tôi vẫn sợ là hiệu suất của việc kiểm tra con trỏ - trong đoạn mã ở trên, đã có 3-4 lệnh gọi API được thực hiện - có thể quá mức cần thiết cho các ứng dụng quan trọng về thời gian.

Sẽ rất tốt nếu ai đó có thể đo lường chi phí kiểm tra con trỏ so với ví dụ: với C # / các cuộc gọi c ++ được quản lý.


1

Thật vậy, điều gì đó có thể được thực hiện trong một số trường hợp cụ thể: ví dụ: nếu bạn muốn kiểm tra xem một chuỗi con trỏ chuỗi có hợp lệ hay không, sử dụng write (fd, buf, szie) syscall có thể giúp bạn làm điều kỳ diệu: hãy để fd là bộ mô tả tệp tạm thời tệp bạn tạo để kiểm tra và buf trỏ đến chuỗi bạn đang kiểm tra, nếu con trỏ không hợp lệ thì write () sẽ trả về -1 và errno được đặt thành EFAULT cho biết buf nằm ngoài vùng địa chỉ có thể truy cập của bạn.


1

Sau đây không hoạt động trong Windows (ai đó đã đề xuất nó trước đây):

 static void copy(void * target, const void* source, int size)
 {
     __try
     {
         CopyMemory(target, source, size);
     }
     __except(EXCEPTION_EXECUTE_HANDLER)
     {
         doSomething(--whatever--);
     }
 }

Hàm phải là phương thức tĩnh, độc lập hoặc tĩnh của một số lớp. Để kiểm tra ở chế độ chỉ đọc, hãy sao chép dữ liệu trong bộ đệm cục bộ. Để kiểm tra khả năng ghi mà không sửa đổi nội dung, hãy viết lại chúng. Bạn chỉ có thể kiểm tra địa chỉ đầu tiên / địa chỉ cuối cùng. Nếu con trỏ không hợp lệ, quyền điều khiển sẽ được chuyển cho 'doSomething', sau đó nằm ngoài dấu ngoặc. Chỉ cần không sử dụng bất kỳ thứ gì yêu cầu trình hủy, như CString.


1

Trên Windows, tôi sử dụng mã này:

void * G_pPointer = NULL;
const char * G_szPointerName = NULL;
void CheckPointerIternal()
{
    char cTest = *((char *)G_pPointer);
}
bool CheckPointerIternalExt()
{
    bool bRet = false;

    __try
    {
        CheckPointerIternal();
        bRet = true;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    return  bRet;
}
void CheckPointer(void * A_pPointer, const char * A_szPointerName)
{
    G_pPointer = A_pPointer;
    G_szPointerName = A_szPointerName;
    if (!CheckPointerIternalExt())
        throw std::runtime_error("Invalid pointer " + std::string(G_szPointerName) + "!");
}

Sử dụng:

unsigned long * pTest = (unsigned long *) 0x12345;
CheckPointer(pTest, "pTest"); //throws exception

0

IsBadReadPtr (), IsBadWritePtr (), IsBadCodePtr (), IsBadStringPtr () dành cho Windows.
Chúng mất thời gian tỷ lệ thuận với độ dài của khối, vì vậy để kiểm tra sự tỉnh táo, tôi chỉ cần kiểm tra địa chỉ bắt đầu.


3
bạn nên tránh những phương pháp này vì chúng không hoạt động. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx
JaredPar 15/02/09

Đôi khi chúng có thể là giải pháp thay thế cho việc chúng không hoạt động: stackoverflow.com/questions/496034/…
ChrisW

0

Tôi đã thấy các thư viện khác nhau sử dụng một số phương pháp để kiểm tra bộ nhớ không tham chiếu và như vậy. Tôi tin rằng họ chỉ đơn giản là "ghi đè" các phương thức phân bổ và phân bổ bộ nhớ (malloc / free), có một số logic theo dõi các con trỏ. Tôi cho rằng điều này là quá mức cần thiết cho trường hợp sử dụng của bạn, nhưng nó sẽ là một cách để làm điều đó.


Thật không may, điều đó không giúp ích gì cho các đối tượng được phân bổ ngăn xếp.
Tom

0

Về mặt kỹ thuật, bạn có thể ghi đè toán tử new (và xóa ) và thu thập thông tin về tất cả bộ nhớ được cấp phát, vì vậy bạn có thể có phương pháp để kiểm tra xem bộ nhớ heap có hợp lệ hay không. nhưng:

  1. bạn vẫn cần một cách để kiểm tra xem con trỏ có được cấp phát trên stack ()

  2. bạn sẽ cần xác định con trỏ 'hợp lệ' là gì:

a) bộ nhớ trên địa chỉ đó được cấp phát

b) vùng nhớ tại địa chỉ đó là địa chỉ bắt đầu của đối tượng (ví dụ: địa chỉ không ở giữa mảng lớn)

c) bộ nhớ tại địa chỉ đó là địa chỉ bắt đầu của đối tượng kiểu mong đợi

Điểm mấu chốt : cách tiếp cận được đề cập không phải là cách C ++, bạn cần xác định một số quy tắc đảm bảo rằng hàm nhận được các con trỏ hợp lệ.



0

Phụ lục cho (các) câu trả lời được tích hợp:

Giả sử rằng con trỏ của bạn chỉ có thể chứa ba giá trị - 0, 1 và -1 trong đó 1 biểu thị một con trỏ hợp lệ, -1 một giá trị không hợp lệ và 0 một giá trị khác không hợp lệ. Xác suất để con trỏ của bạn NULL, tất cả các giá trị đều có khả năng như nhau? 1/3. Bây giờ, hãy loại bỏ trường hợp hợp lệ, vì vậy đối với mọi trường hợp không hợp lệ, bạn có tỷ lệ 50:50 để bắt tất cả các lỗi. Có vẻ tốt phải không? Chia tỷ lệ này cho một con trỏ 4 byte. Có thể có 2 ^ 32 hoặc 4294967294 giá trị. Trong số này, chỉ có MỘT giá trị đúng, một giá trị là NULL, và bạn vẫn còn 4294967292 trường hợp không hợp lệ khác. Tính toán lại: bạn có một bài kiểm tra cho 1 trong số (4294967292+ 1) trường hợp không hợp lệ. Xác suất là 2.xe-10 hoặc 0 cho hầu hết các mục đích thực tế. Đó là sự vô ích của séc NULL.


0

Bạn biết đấy, một trình điều khiển mới (ít nhất là trên Linux) có khả năng này có lẽ sẽ không khó viết như vậy.

Mặt khác, sẽ thật là điên rồ nếu bạn xây dựng các chương trình như thế này. Trừ khi bạn có một số công dụng thực sự cụ thể và duy nhất cho một thứ như vậy, tôi sẽ không khuyên bạn nên dùng nó. Nếu bạn xây dựng một ứng dụng lớn được tải với các kiểm tra tính hợp lệ của con trỏ liên tục, nó có thể sẽ chậm kinh khủng.


0

bạn nên tránh những phương pháp này vì chúng không hoạt động. blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx - JaredPar 15/02/09 lúc 16:02

Nếu chúng không hoạt động - bản cập nhật cửa sổ tiếp theo có khắc phục được không? Nếu chúng không hoạt động ở mức khái niệm - chức năng có thể sẽ bị xóa hoàn toàn khỏi api windows.

Tài liệu MSDN cho rằng chúng bị cấm, và lý do cho điều này có lẽ là sai sót khi thiết kế thêm ứng dụng (ví dụ: nói chung, bạn không nên âm thầm ăn các con trỏ không hợp lệ - tất nhiên nếu bạn phụ trách thiết kế toàn bộ ứng dụng) và hiệu suất / thời gian kiểm tra con trỏ.

Nhưng bạn không nên khẳng định rằng chúng không hoạt động vì một số blog. Trong ứng dụng thử nghiệm của mình, tôi đã xác minh rằng chúng hoạt động.


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.