2009-07-09 25 views
22

Hãy nhìn vào hai chức năng:Trình tự phân bổ biến cục bộ trên stack

void function1() { 
    int x; 
    int y; 
    int z; 
    int *ret; 
} 

void function2() { 
    char buffer1[4]; 
    char buffer2[4]; 
    char buffer3[4]; 
    int *ret; 
} 

Nếu tôi phá vỡ tại function1() trong gdb, và in các địa chỉ của các biến, tôi có được điều này:

(gdb) p &x 
$1 = (int *) 0xbffff380 
(gdb) p &y 
$2 = (int *) 0xbffff384 
(gdb) p &z 
$3 = (int *) 0xbffff388 
(gdb) p &ret 
$4 = (int **) 0xbffff38c 

Nếu tôi làm điều tương tự tại function2(), tôi có được điều này:

(gdb) p &buffer1 
$1 = (char (*)[4]) 0xbffff388 
(gdb) p &buffer2 
$2 = (char (*)[4]) 0xbffff384 
(gdb) p &buffer3 
$3 = (char (*)[4]) 0xbffff380 
(gdb) p &ret 
$4 = (int **) 0xbffff38c 

Bạn sẽ nhận thấy rằng trong cả hai chức năng, ret được lưu trữ gần nhất trên cùng của ngăn xếp. Trong function1(), tiếp theo là z, y và cuối cùng là x. Trong function2(), ret được theo sau bởi buffer1, sau đó buffer2buffer3. Tại sao lệnh lưu trữ thay đổi? Chúng tôi đang sử dụng cùng một lượng bộ nhớ trong cả hai trường hợp (4 byte int s so với 4 byte char mảng), do đó, nó không thể là một vấn đề của đệm. Lý do gì có thể có cho việc sắp xếp lại này, và hơn nữa, có thể bằng cách nhìn vào mã C để xác định trước các biến cục bộ sẽ được sắp xếp không?

Bây giờ tôi biết rằng thông số ANSI cho C không nói gì về thứ tự các biến cục bộ được lưu trữ và trình biên dịch được phép chọn thứ tự riêng của nó, nhưng tôi sẽ tưởng tượng rằng trình biên dịch có quy tắc như thế nào nó sẽ chăm sóc điều này, và giải thích là tại sao những quy tắc đó lại được thực hiện như chúng.

Để tham khảo Tôi đang sử dụng GCC 4.0.1 trên Mac OS 10.5.7

+0

có quan trọng không? bạn có cần các biến được phân bổ trong địa chỉ cụ thể không? – stefanB

+8

Không, nó không quan trọng, chỉ là một bài tập học thuật. – David

+0

Mức độ tối ưu hóa có ảnh hưởng đến câu trả lời không? Đoán thuần túy, nhưng có thể không có/tối ưu hóa thấp, ints là ứng cử viên cho phân bổ đăng ký nhưng char [4] thì không, và vì chúng được xử lý khác nhau, hai cơ chế chỉ xảy ra để đặt chúng trên stack theo các thứ tự khác nhau. Ngay cả khi tối ưu hóa không có sự khác biệt, nó là hợp lý rằng một cái gì đó khác trong cách tự động được xử lý có nghĩa là ints luôn luôn đi xuống một tuyến đường, và mảng luôn luôn xuống khác. –

Trả lời

5

Thông thường nó đã làm với các vấn đề liên kết.

Hầu hết các bộ xử lý đều chậm tìm nạp dữ liệu không được căn chỉnh từ bộ vi xử lý. Họ phải lấy nó thành từng mảnh và ghép nó lại với nhau.

Có lẽ điều đang xảy ra là đặt tất cả các đối tượng lớn hơn hoặc bằng bộ xử lý tối ưu với nhau, và sau đó đóng gói chặt chẽ hơn những thứ có thể không được căn chỉnh. Nó chỉ xảy ra như vậy trong ví dụ của bạn tất cả các mảng char của bạn là 4 byte, nhưng tôi đặt cược nếu bạn đặt chúng là 3 byte, chúng vẫn sẽ kết thúc ở cùng một vị trí.

Nhưng nếu bạn có bốn mảng một byte, chúng có thể kết thúc trong một phạm vi 4 byte hoặc căn chỉnh trong bốn dải riêng biệt.

Đó là tất cả về những gì dễ nhất (chuyển thành "nhanh nhất") để bộ xử lý lấy.

+1

Vâng, ở đây GCC sắp xếp ngăn xếp ở mức 16 byte theo mặc định. Ngoài ra, ngay cả khi chúng tôi đang đối phó với một sự liên kết 4 byte, các mảng và các số nguyên có cùng kích thước (4 byte một phần), vì vậy tôi không biết tại sao bạn muốn sắp xếp lại. – David

0

Tôi đoán là điều này có liên quan đến cách dữ liệu được tải vào sổ đăng ký. Có lẽ, với mảng char, trình biên dịch làm việc một số phép thuật để làm việc song song và điều này có liên quan đến vị trí trong bộ nhớ để dễ dàng tải dữ liệu vào sổ đăng ký. Hãy thử biên dịch với các mức tối ưu hóa khác nhau và thay vào đó hãy thử sử dụng int buffer1[1].

-1

Nó cũng có thể là vấn đề bảo mật?

int main() 
{ 
    int array[10]; 
    int i; 
    for (i = 0; i <= 10; ++i) 
    { 
     array[i] = 0; 
    } 
} 

Nếu mảng thấp hơn chồng trên i, mã này sẽ lặp vô hạn (vì nó truy cập sai và mảng mảng [10], là i).Bằng cách đặt mảng cao hơn trên ngăn xếp, các nỗ lực truy cập bộ nhớ ngoài phần cuối của ngăn xếp sẽ có nhiều khả năng chạm vào bộ nhớ chưa được phân bổ và bị lỗi, thay vì gây ra hành vi không xác định.

Tôi đã thử nghiệm cùng một mã này một lần với gcc và không thể thực hiện được lỗi ngoại trừ kết hợp cờ cụ thể mà tôi không nhớ bây giờ .. Trong mọi trường hợp, nó đặt mảng cách vài byte .

+0

Không có khả năng. Có các trang bảo vệ cho tràn ngăn xếp và tràn, nhưng không có gì giữa các khung ngăn xếp. – lavinio

+1

Vấn đề bảo mật ở đây là mã không chính xác. Có, nó kết quả trong một vòng lặp vô hạn với một trình biên dịch/cờ kết hợp đặc biệt. Nhưng đối với tôi, đó là một sự thoải mái lạnh lùng. –

0

Điều thú vị là nếu bạn thêm một thừa * int2 trong hàm1 thì trên hệ thống của tôi thứ tự là đúng trong khi nó không đúng thứ tự chỉ với 3 biến cục bộ. Đoán của tôi là nó được đặt hàng theo cách đó do phản ánh chiến lược phân bổ đăng ký sẽ được sử dụng. Hoặc là hoặc tùy ý.

14

Tôi không biết why GCC organizes its stack the way it does (mặc dù tôi đoán bạn có thể mở mã nguồn hoặc this paper và tìm hiểu), nhưng tôi có thể cho bạn biết cách đảm bảo thứ tự các biến ngăn xếp cụ thể nếu vì lý do nào đó bạn cần. Chỉ cần đặt chúng vào cấu trúc:

void function1() { 
    struct { 
     int x; 
     int y; 
     int z; 
     int *ret; 
    } locals; 
} 

Nếu bộ nhớ của tôi phục vụ cho tôi đúng, thông số kỹ thuật đảm bảo rằng &ret > &z > &y > &x. Tôi để lại K & R tại nơi làm việc vì vậy tôi không thể trích dẫn chương và câu.

6

ISO C không chỉ nói gì về thứ tự của các biến cục bộ trên ngăn xếp, nó thậm chí còn không đảm bảo rằng một ngăn xếp thậm chí tồn tại. Tiêu chuẩn chỉ nói về phạm vi và tuổi thọ của các biến bên trong một khối.

0

Hoàn toàn tùy thuộc vào trình biên dịch. Ngoài ra, các biến thủ tục nhất định có thể không bao giờ được đặt trên ngăn xếp, vì chúng có thể dành toàn bộ cuộc sống của chúng trong một thanh ghi.

7

Vì vậy, tôi đã làm một số thử nghiệm khác và đây là những gì tôi đã tìm thấy. Dường như dựa trên việc mỗi biến có phải là một mảng hay không. Với đầu vào này:

void f5() { 
     int w; 
     int x[1]; 
     int *ret; 
     int y; 
     int z[1]; 
} 

tôi kết thúc với điều này trong gdb:

(gdb) p &w 
$1 = (int *) 0xbffff4c4 
(gdb) p &x 
$2 = (int (*)[1]) 0xbffff4c0 
(gdb) p &ret 
$3 = (int **) 0xbffff4c8 
(gdb) p &y 
$4 = (int *) 0xbffff4cc 
(gdb) p &z 
$5 = (int (*)[1]) 0xbffff4bc 

Trong trường hợp này, int s và con trỏ được giải quyết đầu tiên, tuyên bố cuối cùng trên đỉnh của ngăn xếp và lần đầu tiên tuyên bố gần phía dưới. Sau đó, các mảng được xử lý, theo hướng ngược lại, khai báo trước đó, mức cao nhất trên stack. Tôi chắc rằng có một lý do chính đáng cho việc này. Tôi tự hỏi nó là cái gì.

1

Chuẩn C không quy định bất kỳ bố cục nào cho các biến tự động khác. Tuy nhiên, đặc biệt, để tránh nghi ngờ, rằng

[...] Bố cục lưu trữ cho tham số [chức năng] không được chỉ định. (C11 6.9.1p9)

Nó có thể được hiểu từ đó mà ông bố trí lượng lưu trữ cho bất kỳ đối tượng khác là tương tự như vậy không xác định, trừ trường hợp đối với số ít những yêu cầu được đưa ra bởi tiêu chuẩn, trong đó có các con trỏ null không thể trỏ đến bất kỳ đối tượng hợp lệ hoặc hàm và bố trí trong các đối tượng tổng hợp.

Chuẩn C không chứa đơn lẻ đề cập đến từ "ngăn xếp"; nó là khá có thể làm ví dụ một thực hiện C là stackless, phân bổ mỗi hồ sơ kích hoạt từ đống (mặc dù những có thể sau đó có lẽ được hiểu để tạo thành một chồng).

Một trong những lý do khiến trình biên dịch mất nhiều thời gian là hiệu quả. Tuy nhiên, các trình biên dịch hiện tại cũng sẽ sử dụng tính năng này để bảo mật, sử dụng các thủ thuật như ngẫu nhiên bố cục không gian địa chỉ và stack canaries để cố gắng khai thác hành vi không xác định khó khăn hơn. Việc sắp xếp lại các bộ đệm được thực hiện để làm cho việc sử dụng canary hiệu quả hơn.

Các vấn đề liên quan