Cụ thể, điều gì nguy hiểm về việc truyền kết quả của malloc?


86

Bây giờ trước khi mọi người bắt đầu đánh dấu điều này là trùng lặp, tôi đã đọc tất cả những điều sau đây, không có câu nào cung cấp câu trả lời mà tôi đang tìm kiếm:

  1. C Câu hỏi thường gặp: Có gì sai với việc truyền giá trị trả về của malloc?
  2. VẬY: Tôi có nên ép kiểu rõ ràng giá trị trả về của malloc () không?
  3. VẬY: Phôi con trỏ không cần thiết trong C
  4. VẬY: Tôi có bỏ kết quả của malloc không?

Cả C FAQ và nhiều câu trả lời cho các câu hỏi trên đều trích dẫn một lỗi bí ẩn mà mallocgiá trị trả về của ép kiểu có thể ẩn; tuy nhiên, không ai trong số họ đưa ra một ví dụ cụ thể về một lỗi như vậy trong thực tế. Bây giờ hãy chú ý rằng tôi đã nói lỗi , không phải cảnh báo .

Bây giờ được cung cấp mã sau:

#include <string.h>
#include <stdio.h>
// #include <stdlib.h>

int main(int argc, char** argv) {

    char * p = /*(char*)*/malloc(10);
    strcpy(p, "hello");
    printf("%s\n", p);

    return 0;
}

Việc biên dịch đoạn mã trên với gcc 4.2, có và không có ép kiểu đưa ra các cảnh báo giống nhau và chương trình thực thi đúng cách và cung cấp kết quả giống nhau trong cả hai trường hợp.

anon@anon:~/$ gcc -Wextra nostdlib_malloc.c -o nostdlib_malloc
nostdlib_malloc.c: In function ‘main’:
nostdlib_malloc.c:7: warning: incompatible implicit declaration of built-in function ‘malloc’
anon@anon:~/$ ./nostdlib_malloc 
hello

Vì vậy, bất cứ ai có thể cung cấp một ví dụ mã cụ thể về lỗi biên dịch hoặc thời gian chạy có thể xảy ra do mallocgiá trị trả về của ép kiểu, hay đây chỉ là một huyền thoại đô thị?

Chỉnh sửa Tôi đã bắt gặp hai lập luận bằng văn bản liên quan đến vấn đề này:

  1. Trong Ưu đãi Truyền: Tư vấn CERT: Truyền ngay kết quả của một lệnh gọi hàm cấp phát bộ nhớ thành một con trỏ đến kiểu được cấp phát
  2. Chống truyền (lỗi 404 kể từ 2012-02-14: sử dụng bản sao Máy lưu trữ Internet từ 2010-01-27. {2016-03-18: "Không thể thu thập dữ liệu hoặc hiển thị trang do robots.txt."})

6
đúc voidcon trỏ cho phép để biên dịch mã như C ++; một số người nói rằng đó là một tính năng, tôi muốn nói đó là một lỗi;)
Christoph

1
ngoài ra, hãy đọc các nhận xét ở đầu liên kết của bạn vì nó mô tả những gì bạn nên làm thay vì truyền: securecoding.cert.org/confluence/display/seccode/…
Christoph

3
Tôi sẽ lấy lời khuyên của CERTs để bao gồm cả dàn diễn viên. Ngoài ra, tôi sẽ không quên bao gồm stdlib.h. :)
Abhinav

1
Đây là một ví dụ SO về lỗi thời gian chạy biên dịch do mallocgiá trị trả về của ép kiểu: truyền đến int*trên vòm 64-bit.
John_West

1
Câu hỏi này Ckhông được gắn thẻ C++(chúng là hai ngôn ngữ khác nhau) Vì vậy, bất kỳ cuộc thảo luận nào (như trong một số câu trả lời) đều không liên quan đến câu hỏi này.
user3629249 Ngày

Câu trả lời:


66

Bạn sẽ không gặp lỗi trình biên dịch mà là cảnh báo trình biên dịch . Như các nguồn bạn trích dẫn đã nói (đặc biệt là nguồn đầu tiên ), bạn có thể gặp lỗi thời gian chạy không thể đoán trước khi sử dụng truyền mà không bao gồmstdlib.h .

Như vậy lỗi ở phía bạn không phải là diễn viên mà quên đưa vào stdlib.h. Trình biên dịch có thể giả định rằng malloclà một hàm trả về int, do đó chuyển đổi void*con trỏ thực sự được trả về bởi mallocđến intvà sau đó đến kiểu con trỏ của bạn do các diễn viên rõ ràng. Trên một số nền tảng intvà con trỏ có thể chiếm nhiều byte khác nhau, do đó, việc chuyển đổi kiểu có thể dẫn đến hỏng dữ liệu.

May mắn thay, các trình biên dịch hiện đại đưa ra các cảnh báo chỉ ra lỗi thực sự của bạn. Xem kết gccquả đầu ra bạn đã cung cấp: Nó cảnh báo bạn rằng khai báo ngầm định ( int malloc(int)) không tương thích với cài đặt sẵn malloc. Vì vậy, gccdường như biết mallocngay cả khi không có stdlib.h.

Bỏ diễn viên để tránh lỗi này chủ yếu là lý do giống như viết

if (0 == my_var)

thay vì

if (my_var == 0)

vì lỗi thứ hai có thể dẫn đến một lỗi nghiêm trọng nếu người ta nhầm lẫn ===, trong khi lỗi đầu tiên sẽ dẫn đến lỗi biên dịch. Cá nhân tôi thích phong cách thứ hai hơn vì nó phản ánh tốt hơn ý định của tôi và tôi không có xu hướng mắc phải sai lầm này.

Điều này cũng đúng với việc truyền giá trị được trả về bởi malloc: Tôi thích rõ ràng trong lập trình và tôi thường kiểm tra kỹ để bao gồm các tệp tiêu đề cho tất cả các hàm tôi sử dụng.


2
Có vẻ như vì trình biên dịch cảnh báo về khai báo ngầm không tương thích, nên đây không phải là vấn đề miễn là bạn chú ý đến cảnh báo trình biên dịch của mình.
Robert S. Barnes

4
@Robert: vâng, đã đưa ra các giả định nhất định về trình biên dịch. Khi mọi người đưa ra lời khuyên về cách viết C tốt nhất nói chung , họ không thể cho rằng người nhận lời khuyên đang sử dụng phiên bản gcc gần đây.
Steve Jessop

4
Ồ, và câu trả lời cho câu hỏi thứ hai là người gọi có chứa mã để nhận giá trị trả về (mà nó nghĩ là một số nguyên) và chuyển nó thành T *. Callee chỉ ghi giá trị trả về (dưới dạng void *) và trả về. Vì vậy, tùy thuộc vào quy ước gọi: int trả về và trả về void * có thể có hoặc không ở cùng một nơi (thanh ghi hoặc ngăn xếp); int và void * có thể có hoặc không cùng kích thước; chuyển đổi giữa hai có thể có hoặc có thể không. Vì vậy, nó có thể "chỉ hoạt động", hoặc giá trị có thể bị hỏng (có lẽ bị mất một số bit), hoặc người gọi có thể nhận hoàn toàn giá trị sai.
Steve Jessop

1
@ RobertS.Barnes đến muộn, nhưng: Giá trị trả về thường không phải là một phần của chữ ký hàm, thậm chí không phải trong C ++. Trình liên kết chỉ tạo ra một bước nhảy đến một biểu tượng, vậy thôi.
Peter - Phục hồi Monica

3
Bạn có thể gặp lỗi thời gian chạy không thể đoán trước khi sử dụng truyền mà không bao gồm stdlib.h . Điều đó đúng, nhưng không bao gồm tự stdlib.hnó đã là một lỗi, ngay cả khi bạn chỉ nhận được cảnh báo "khai báo ẩn ý".
Jabberwocky

45

Một trong những lập luận tốt của cấp cao hơn chống lại việc bỏ kết quả mallocthường không được đề cập, mặc dù theo tôi, nó quan trọng hơn các vấn đề cấp dưới đã biết (như cắt bớt con trỏ khi thiếu phần khai báo).

Một phương pháp lập trình tốt là viết mã, mã này càng độc lập với kiểu càng tốt. Điều này có nghĩa là, đặc biệt, tên loại nên được đề cập trong mã càng ít càng tốt hoặc tốt nhất là không được đề cập đến. Điều này áp dụng cho các kiểu (tránh các kiểu không cần thiết), các kiểu làm đối số của sizeof(tránh sử dụng tên kiểu trong sizeof) và nói chung, tất cả các tham chiếu khác đến tên kiểu.

Tên loại thuộc về khai báo. Càng nhiều càng tốt, tên kiểu nên được hạn chế cho các khai báo và chỉ cho các khai báo.

Theo quan điểm này, đoạn mã này là xấu

int *p;
...
p = (int*) malloc(n * sizeof(int));

và điều này tốt hơn nhiều

int *p;
...
p = malloc(n * sizeof *p);

không đơn giản vì nó "không truyền kết quả của malloc", mà là vì nó không phụ thuộc vào kiểu (hoặc kiểu-agnositic, nếu bạn thích), vì nó tự động điều chỉnh theo bất kỳ kiểu nào pđược khai báo, mà không cần bất kỳ sự can thiệp nào từ người dùng.


Fwiw, tôi nghĩ rằng đây ít nhiều là lý do giống như thế này: stackoverflow.com/questions/953112/… nhưng tập trung vào tính độc lập của kiểu thay vì tự làm. Tất nhiên cái đầu tiên theo sau cái thứ hai (hoặc ngược lại), vì vậy ít nhất đôi khi nó được đề cập đến . :)
thư giãn

5
@unwind bạn rất có thể có nghĩa là khô chứ không phải DIY
kratenko

18

Các hàm không phải nguyên mẫu được giả định là trả về int.

Vì vậy, bạn đang truyền intmột con trỏ tới một con trỏ. Nếu con trỏ rộng hơn ints trên nền tảng của bạn, thì đây là hành vi có rủi ro cao.

Ngoài ra, tất nhiên, một số người coi cảnh báo lỗi, tức là mã sẽ được biên dịch mà không có chúng.

Cá nhân tôi nghĩ rằng thực tế là bạn không cần phải truyền void *đến một loại con trỏ khác là một tính năng trong C và hãy xem xét mã không bị hỏng.


14
Tôi tin rằng trình biên dịch biết nhiều về ngôn ngữ hơn tôi, vì vậy nếu nó cảnh báo tôi về điều gì đó, tôi sẽ chú ý.
György Andrasek, 14/10/09

3
Trong nhiều dự án, mã C được biên dịch dưới dạng C ++, nơi bạn cần truyền void*.
laalto 14/10/09

nit: " theo mặc định , các hàm không phải nguyên mẫu được giả định là trả về int." - Ý bạn là có thể thay đổi kiểu trả về của các hàm không phải nguyên mẫu?
pmg 14/10/09

1
@laalto - Đúng vậy, nhưng không nên. C là C, không phải C ++ và phải được biên dịch bằng trình biên dịch C, không phải trình biên dịch C ++. Không có lý do gì: GCC (một trong những trình biên dịch C tốt nhất hiện có) chạy trên hầu hết mọi nền tảng có thể tưởng tượng được (và tạo ra mã được tối ưu hóa cao). Những lý do nào khiến bạn có thể phải biên dịch C bằng trình biên dịch C ++, ngoài sự lười biếng và các tiêu chuẩn lỏng lẻo?
Chris Lutz 14/10/09

3
Ví dụ về mã mà bạn có thể muốn biên dịch như cả C và C ++: #ifdef __cplusplus \nextern "C" { \n#endif static inline uint16_t swb(uint16_t a) {return ((a << 8) | ((a >> 8) & 0xFF); } \n#ifdef __cplusplus\n } \n#endif. Bây giờ, tại sao bạn muốn gọi malloc trong một hàm nội tuyến tĩnh, tôi thực sự không biết, nhưng các tiêu đề hoạt động trong cả hai thì hầu như chưa từng nghe đến.
Steve Jessop

11

Nếu bạn làm điều này khi biên dịch ở chế độ 64-bit, con trỏ trả về của bạn sẽ bị cắt ngắn thành 32-bit.

EDIT: Xin lỗi vì quá ngắn gọn. Đây là một đoạn mã ví dụ cho mục đích thảo luận.

chủ yếu()
{
   char * c = (char *) malloc (2);
   printf ("% p", c);
}

Giả sử rằng con trỏ heap được trả về là một cái gì đó lớn hơn những gì có thể biểu diễn trong một int, giả sử 0xAB00000000.

Nếu malloc không được tạo nguyên mẫu để trả về một con trỏ, giá trị int được trả về ban đầu sẽ nằm trong một số thanh ghi với tất cả các bit quan trọng được đặt. Bây giờ trình biên dịch nói, "được rồi, làm cách nào để chuyển đổi và int thành một con trỏ". Đó sẽ là phần mở rộng dấu hiệu hoặc phần mở rộng bằng không của 32-bit bậc thấp mà nó đã được thông báo cho malloc là "trả về" bằng cách bỏ qua nguyên mẫu. Vì int được ký nên tôi nghĩ rằng chuyển đổi sẽ là phần mở rộng dấu, trong trường hợp này sẽ chuyển đổi giá trị thành 0. Với giá trị trả về 0xABF0000000, bạn sẽ nhận được một con trỏ khác 0, điều này cũng sẽ gây ra một số điều thú vị khi bạn cố gắng bỏ qua nó.


1
Bạn có thể giải thích chi tiết điều này sẽ xảy ra như thế nào?
Robert S. Barnes

5
tôi nghĩ rằng Peeter Joot đã tìm ra rằng "Theo mặc định, các hàm không phải nguyên mẫu được giả định trả về int" w / o bao gồm stdlib.h và sizeof (int) là 32 bit trong khi sizeof (ptr) là 64.
Kiểm tra

4

Quy tắc phần mềm có thể tái sử dụng:

Trong trường hợp viết một hàm nội tuyến trong đó sử dụng malloc (), để làm cho nó cũng có thể sử dụng lại cho mã C ++, vui lòng thực hiện ép kiểu rõ ràng (ví dụ: (char *)); nếu không trình biên dịch sẽ phàn nàn.


hy vọng rằng với việc (gần đây) bao gồm tối ưu hóa thời gian liên kết trong gcc (xem gcc.gnu.org/ml/gcc/2009-10/msg00060.html ), việc khai báo hàm nội tuyến trong tệp tiêu đề sẽ không còn cần thiết nữa
Christoph

bạn có những ý tưởng tồi. bạn có biết rằng những gì là di động và đa nền tảng giữa các trình biên dịch / phiên bản / kiến ​​trúc khác nhau? ok, bạn có thể không. sau đó tái sử dụng nghĩa là gì?
Kiểm tra

2
khi viết C ++, malloc / free KHÔNG phải là phương pháp phù hợp. Thay vì sử dụng mới / xóa. IE không được phép gọi / nada / zero đến malloc / free trong mã C ++
user3629249

3
@ user3629249: Khi viết một chức năng mà cần phải được sử dụng từ bên trong hoặc mã C hoặc C ++, sử dụng malloc/ freecho cả là apt là tốt hơn so với cố gắng sử dụng malloctrong C và newC ++, đặc biệt là nếu cấu trúc dữ liệu được chia sẻ giữa C và C ++ mã và có khả năng một đối tượng có thể được tạo bằng mã C và được phát hành bằng mã C ++ hoặc ngược lại.
supercat

3

Một con trỏ void trong C có thể được gán cho bất kỳ con trỏ nào mà không có kiểu ép kiểu rõ ràng. Trình biên dịch sẽ đưa ra cảnh báo nhưng nó có thể được sử dụng lại trong C ++ bằng cách ép kiểu malloc()sang kiểu tương ứng. Với đúc kiểu out nó cũng có thể được sử dụng trong C , vì C không phải là loại kiểm tra nghiêm ngặt . Nhưng C ++ là kiểm tra kiểu nghiêm ngặt nên cần phải nhập kiểu ép kiểu malloc()trong C ++.


Nếu bạn sử dụng malloc trong C ++, bạn nên có một lý do chính đáng! ; p
antred
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.