2015-05-22 20 views
5

Giả sử chúng ta có đoạn mã sau:Hạn chế truy cập lĩnh vực giữa hai đối tượng cùng loại trong gcc

typedef struct { 
    int f1; 
    int f2; 
} t_str; 

int f(t_str* p, t_str* q) 
{ 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 
    p[0].f1++; 
    q[0].f2++; 

return 0; 
} 

Khi chúng tôi biên dịch nó (tôi đã sử dụng gcc-5.1.0) với O3 tùy chọn, trình biên dịch được người lắp ráp sau đây:

f: 
.LFB0: 
    .cfi_startproc 
    movl 8(%esp), %edx 
    movl 4(%esp), %ecx 
    movl 4(%edx), %eax 
    addl $9, (%ecx) 
    addl $9, %eax 
    movl %eax, 4(%edx) 
    xorl %eax, %eax 
    ret 
    .cfi_endproc 

Điều đó có nghĩa là gcc đã quyết định truy cập vào trường f1 của p và truy cập vào trường f2 của q không bao giờ là bí danh. Tôi đoán điều này xuất phát từ giả định rằng hai đối tượng cùng loại không bao giờ chồng lên nhau hoặc chúng giống nhau. Nhưng tôi không tìm thấy vấn đề trong tiêu chuẩn.

Vì vậy, vui lòng, bất kỳ ai có thể tìm thấy vấn đề này theo tiêu chuẩn hoặc một điểm khác tại sao gcc giới hạn quyền truy cập trường hoặc nhận xét điều gì đã xảy ra?

UPD:

Vâng, tôi nghĩ về khoản 7 của Phần 6.5 quá, nhưng nó sẽ được thoải mái hơn cho tôi để có một cái gì đó như thế ở dạng rõ ràng cho tất cả các đối tượng:

6,5. 16,1 Simple phân

3 Nếu giá trị được lưu trữ trong một đối tượng được đọc từ một đối tượng chồng lấn trong bất kỳ cách nào lưu trữ các đối tượng đầu tiên, sau đó các chồng chéo sẽ là chính xác và hai đối tượng có trách nhiệm đủ điều kiện hoặc phiên bản không đủ tiêu chuẩn của loại tương thích; nếu không, hành vi là không xác định.

Rất tiếc, không thể sử dụng quy tắc này tại đây.

Bây giờ nhìn, nếu vì mã trên tôi thực hiện các chức năng sau:

void main() 
{ 
    char * c = malloc(12); 
    memset(c, 0, 12); 
    f((t_str *)(c + 4), (t_str *)c); 
    printf("%d %d %d\n", ((t_str *)c)->f1, ((t_str *)c)->f2, ((t_str *)(c + 4))->f2); 
} 

Bây giờ tôi nhận được sau khi thực hiện:

$ gcc-5.1.0 test1.c -O3 && ./a.out 
0 9 0 
$ gcc-5.1.0 test1.c -O0 && ./a.out 
0 18 0 

Vậy làm thế nào bạn nghĩ là mã này có hiệu lực? Bởi vì tôi không shure nếu nó statisfy Khoản 7 của Mục 6.5.

PS: điều thú vị:

$ gcc-5.1.0 test1.c -O3 -fwhole-program && ./a.out 
0 10 0 
$ gcc-5.1.0 test1.c -O3 -flto && ./a.out 
0 10 0 
+0

Tôi không chắc tôi hiểu câu hỏi của bạn. Bạn có ý nghĩa gì với "bí danh" và "trùng lặp"? Trình biên dịch có vẻ đúng như thế nào, miễn là p và q không thay đổi giữa các lần truy cập (ví dụ: thông qua việc xử lý ngắt hoặc như vậy). Tôi nghĩ bạn có thể sử dụng từ khóa "dễ bay hơi" để buộc trình biên dịch cho rằng chúng có thể đã thay đổi. –

+0

Để có thêm niềm vui, hãy xem điều gì xảy ra nếu bạn khai báo 'int * pf1 = &p->f1; int * pf2 = &p->f2;' vv và bây giờ thay thế 'p-> f1 ++' bằng '(* pf1) ++'. –

+0

alexanius tự hỏi liệu tiêu chuẩn có loại trừ khả năng 'q' trỏ tới' p-> f2' hay không. Vì cấu trúc 't_str' chỉ có 2 thành viên' int', một mảng gồm 2 cấu trúc như vậy có thể được xem là có chứa một cấu trúc thứ ba như vậy tại địa chỉ '& p-> f2'.Tôi giả vờ rằng nó được loại trừ, nhưng nó có thể là khó khăn hơn để làm cho một trường hợp tương tự cho 'typedef struct {int f [2]; } t_str; ' – chqrlie

Trả lời

3

Đoạn 7 của Phần 6.5 của dự thảo mới nhất (N1570) của C11 đọc:

Một đối tượng có trách nhiệm đã có giá trị được lưu trữ của nó chỉ được truy cập bởi một biểu thức vế trái có một trong các loại sau: 88) - loại tương thích với loại đối tượng hiệu quả, - phiên bản đủ điều kiện của loại tương thích với loại đối tượng hiệu quả, - loại được ký hoặc chưa ký loại tương ứng với effe loại ctive của đối tượng, - loại là loại đã ký hoặc chưa ký tương ứng với phiên bản đủ điều kiện của loại đối tượng hiệu quả, - loại tổng hợp hoặc liên kết bao gồm một trong các loại được nêu trên trong số các thành viên của nó (bao gồm, đệ quy, một thành viên của một tổ hợp hoặc công đoàn), hoặc - một loại ký tự.

tôi giải thích này có nghĩa là đối tượng được trỏ đến bởi pq không thể chồng lên nhau trừ khi họ là cùng một đối tượng, bởi vì các đối tượng t_str có nghĩa vụ phải được truy cập bởi con trỏ thích hợp.

Các tiêu chuẩn là không đủ chính xác để làm cho nó rõ ràng rằng &p->f2 không phải là một con trỏ hợp lệ cho một đối tượng t_str gồm 2 int chia sẻ giữa p[0]p[1]. Tuy nhiên, có vẻ như không chính xác vì trình biên dịch có thể chèn đệm giữa f1f2 hoặc thực sự giữa f2 và kết thúc của cấu trúc.

Ngẫu nhiên, &p->f2 - &p->f1 không phải là một biểu thức hợp lệ vì đoạn 9 của phần 6.5.6 khai thác bang Additive ràng buộc này: Khi hai con trỏ được trừ, cả hai sẽ trỏ đến các yếu tố của đối tượng cùng một mảng, hoặc một quá khứ yếu tố cuối cùng của đối tượng mảng;

Nếu chức năng f() mất một con trỏ đến char như tham số và dữ liệu truy cập thông qua con trỏ này, gcc không thể giả định dữ liệu này được phân biệt với các int thành viên của cấu trúc được trỏ đến bởi pq. Ngoại lệ hơi ngược lại này là lý do tại sao rất nhiều nguyên mẫu chức năng của thư viện C có số vòng loại restrict trên nhiều đối số con trỏ. (Những vòng loại này trong các nguyên mẫu hàm chỉ là một gợi ý cho lập trình viên, nhưng không thực sự nói với trình biên dịch bất cứ điều gì).

+0

Cảm ơn, tôi đã cập nhật câu hỏi gốc – alexanius

0

Như chqrlie đã đề cập, có khả năng gcc sẽ không cho phép bạn tạo tình huống tại đó &p[0].f1 == &q[0].f2. Điều đó đang được nói, nó không nên tạo nên sự khác biệt. Những hoạt động đó đi lại, tính toán. Và cách duy nhất sẽ phá vỡ trong mã là nếu có một tràn số nguyên, nơi một lần nữa thứ tự không quan trọng. Tôi đã thử nghiệm điều này và cho trình biên dịch của tôi (gcc 3.4.6), tối ưu hóa này xuất hiện ngay sau khi tôi bật bất kỳ tối ưu hóa nào.

Là một đối trọng, xem xét những gì sẽ xảy ra khi chúng ta thay đổi cấu trúc để

typedef struct { 
    double f1; 
    double f2; 
} t_str; 

Trong trường hợp này, ngay cả với O3, chúng ta không thấy sự gia tăng được kết hợp với nhau, bởi vì chúng tôi có thể nhận được câu trả lời khác nhau do để tràn.

+0

Trong khi tôi đồng ý rằng hoạt động đi làm trong nguồn C, asm được tạo ra bởi 'gcc' phá vỡ điều này bằng cách nạp'% eax' trước khi tăng '(% ecx)' và lưu trữ '% eax + 9' ​​sau đó. Một asm thường xuyên hơn với 'addl $ 9, (% edx)' sẽ không đưa ra giả định rằng '* p' và' * q' không chồng lên nhau. – chqrlie

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