Tại sao GCC pad hoạt động với NOP?


81

Tôi đã làm việc với C một thời gian ngắn và gần đây đã bắt đầu tham gia vào ASM. Khi tôi biên dịch một chương trình:

Quá trình gỡ bỏ objdump có mã, nhưng không đạt sau lần sửa lại:

Từ những gì tôi học được, nops không làm gì cả, và vì sau khi ret thậm chí sẽ không được thực hiện.

Câu hỏi của tôi là: tại sao phải bận tâm? ELF (linux-x86) không thể hoạt động với phần .text (+ main) ở bất kỳ kích thước nào?

Tôi đánh giá cao mọi sự giúp đỡ, chỉ cố gắng học hỏi.


Những NOP đó có tiếp tục không? Nếu chúng dừng lại 80483af, thì có thể đó là đệm để căn chỉnh hàm tiếp theo thành 8 hoặc 16 byte.
Mysticial

không sau 4 nops nó đi ngang qua một chức năng: __libc_csu_fini
olly

1
Nếu NOPs được chèn vào bởi gcc sau đó tôi không nghĩ rằng nó sẽ chỉ sử dụng 0x90 kể từ khi có nhiều NOPs với biến kích thước từ 1-9 byte (10 nếu sử dụng cú pháp khí )
phuclv

Câu trả lời:


89

Trước hết, gcckhông phải lúc nào cũng làm điều này. Phần đệm được điều khiển bởi -falign-functions, được tự động bật bởi -O2-O3:

-falign-functions
-falign-functions=n

Căn chỉnh phần bắt đầu của các hàm thành lũy thừa tiếp theo của hai lớn hơn n, bỏ qua tối đa nbyte. Ví dụ: -falign-functions=32căn chỉnh các chức năng với ranh giới 32 byte tiếp theo, nhưng -falign-functions=24sẽ căn chỉnh với ranh giới 32 byte tiếp theo chỉ khi điều này có thể được thực hiện bằng cách bỏ qua 23 byte hoặc ít hơn.

-fno-align-functions-falign-functions=1tương đương và có nghĩa là các hàm sẽ không được căn chỉnh.

Một số bộ lắp ráp chỉ hỗ trợ cờ này khi n là lũy thừa của hai; trong trường hợp đó, nó được làm tròn.

Nếu n không được chỉ định hoặc bằng 0, hãy sử dụng mặc định phụ thuộc vào máy.

Được kích hoạt ở mức -O2, -O3.

Có thể có nhiều lý do để làm điều này, nhưng lý do chính trên x86 có lẽ là:

Hầu hết các bộ xử lý tìm nạp các hướng dẫn trong các khối 16 byte hoặc 32 byte được căn chỉnh. Có thể thuận lợi khi căn chỉnh các mục của vòng lặp quan trọng và các mục của chương trình con bằng 16 để giảm thiểu số lượng ranh giới 16 byte trong mã. Ngoài ra, hãy đảm bảo rằng không có ranh giới 16 byte trong vài lệnh đầu tiên sau một mục nhập vòng lặp quan trọng hoặc mục nhập chương trình con.

(Trích từ "Tối ưu hóa chương trình con trong hợp ngữ" của Agner Fog.)

chỉnh sửa: Đây là một ví dụ minh họa padding:

Khi được biên dịch bằng gcc 4.4.5 với cài đặt mặc định, tôi nhận được:

Chỉ định -falign-functionscho:


1
Tôi không sử dụng bất kỳ cờ -O nào, đơn giản là "gcc -o test test.c".
olly

1
@olly: Tôi đã thử nghiệm nó với gcc 4.4.5 trên Ubuntu 64-bit và trong các thử nghiệm của tôi không có phần đệm theo mặc định và có phần đệm với -falign-functions.
NPE

@aix: Tôi đang sử dụng centOS 6.0 (32-bit) và không có bất kỳ cờ nào có phần đệm. Bất cứ ai muốn tôi kết xuất đầu ra "objdump -j .text -d ./test" đầy đủ của tôi?
olly

1
Khi thử nghiệm thêm, khi tôi biên dịch nó thành một đối tượng: "gcc -c test.c". Không có phần đệm, nhưng khi tôi liên kết: "gcc -o test test.o" thì nó xuất hiện.
olly

2
@olly: Phần đệm đó được trình liên kết chèn vào, để đáp ứng các yêu cầu căn chỉnh của hàm theo sau maintrong tệp thực thi (trong trường hợp của tôi là hàm đó __libc_csu_fini).
NPE

15

Điều này được thực hiện để căn chỉnh hàm tiếp theo theo ranh giới 8, 16 hoặc 32 byte.

Từ “Tối ưu hóa chương trình con trong hợp ngữ” của A.Fog:

11.5 Căn chỉnh mã

Hầu hết các bộ vi xử lý tìm nạp mã trong các khối 16 byte hoặc 32 byte được căn chỉnh. Nếu một mục nhập chương trình con quan trọng hoặc nhãn nhảy xảy ra ở gần cuối của khối 16 byte thì bộ xử lý của chúng sẽ chỉ nhận được một vài byte mã hữu ích khi tìm nạp khối mã đó. Nó có thể phải tìm nạp 16 byte tiếp theo trước khi nó có thể giải mã các hướng dẫn đầu tiên sau nhãn. Điều này có thể tránh được bằng cách căn chỉnh các mục nhập chương trình con quan trọng và các mục vòng lặp theo 16.

[...]

Căn chỉnh một mục chương trình con cũng đơn giản như đặt càng nhiều NOP trước mục chương trình con càng tốt để làm cho địa chỉ chia hết cho 8, 16, 32 hoặc 64, như mong muốn.


Đó là sự khác biệt giữa 25-29 byte (cho chính), bạn đang nói về điều gì đó lớn hơn? Giống như phần văn bản, thông qua bản thân tôi thấy nó là 364 byte? Tôi cũng nhận thấy 14 nút trên _start. Tại sao "as" không làm những điều này? Tôi là tân binh, xin lỗi.
olly

@olly: Tôi đã thấy các hệ thống phát triển thực hiện tối ưu hóa toàn bộ chương trình trên mã máy đã biên dịch. Nếu địa chỉ của hàm foolà 0x1234, thì mã xảy ra sử dụng địa chỉ đó gần với chữ 0x1234 có thể kết thúc tạo ra mã máy mov ax,0x1234 / push ax / mov ax,0x1234 / push axmà sau đó trình tối ưu hóa có thể thay thế bằng mov ax,0x1234 / push ax / push ax. Lưu ý rằng các chức năng không được di chuyển lại sau khi tối ưu hóa như vậy, vì vậy việc loại bỏ các lệnh sẽ cải thiện tốc độ thực thi, nhưng không cải thiện kích thước mã.
supercat

5

Theo như tôi nhớ, các hướng dẫn được đặt trong cpu và các khối cpu khác nhau (bộ tải, bộ giải mã, v.v.) xử lý các hướng dẫn tiếp theo. Khi các REThướng dẫn đang được thực thi, một số hướng dẫn tiếp theo đã được tải vào đường ống cpu. Đó là một phỏng đoán, nhưng bạn có thể bắt đầu đào tại đây và nếu bạn tìm ra (có thể là số lượng cụ thể NOPan toàn, vui lòng chia sẻ phát hiện của bạn.


@ninjalj: Hả? Câu hỏi này đang hỏi về x86, được kết nối với nhau (như mco đã nói). Nhiều bộ xử lý x86 hiện đại cũng thực thi các lệnh "không nên" một cách phỏng đoán, có lẽ bao gồm cả các lệnh này. Có lẽ bạn muốn bình luận ở nơi khác?
David Cary

3
@DavidCary: trong x86, điều đó hoàn toàn minh bạch đối với lập trình viên. Các hướng dẫn được thực thi theo suy đoán được đoán sai sẽ khiến kết quả và hiệu ứng của chúng bị loại bỏ. Trên MIPS, không có phần "đầu cơ" nào cả, lệnh trong khe trễ nhánh luôn được thực thi và lập trình viên phải lấp đầy các khe trễ (hoặc để trình hợp dịch thực hiện, điều này có thể dẫn đến nops).
ninjalj

@ninjalj: Có, ảnh hưởng của các hoạt động được thực thi theo suy đoán bị đoán sai và các lệnh không căn chỉnh là minh bạch, theo nghĩa là chúng không ảnh hưởng đến giá trị dữ liệu đầu ra. Tuy nhiên, cả hai đều có ảnh hưởng đến thời gian của chương trình, đó có thể là lý do gcc thêm nops vào mã x86, đó là những gì câu hỏi ban đầu được hỏi.
David Cary

1
@DavidCary: nếu đó là lý do, bạn sẽ chỉ thấy nó sau khi nhảy có điều kiện, không phải sau vô điều kiện ret.
ninjalj

1
Đây không phải là lý do tại sao. Dự đoán dự phòng của bước nhảy gián tiếp (khi bỏ lỡ BTB) là hướng dẫn tiếp theo, nhưng nếu đó là lỗi không phải hướng dẫn, thì tối ưu hóa được đề xuất để ngăn suy đoán sai là một hướng dẫn giống như ud2hoặc int3luôn lỗi, vì vậy giao diện người dùng biết để dừng giải mã thay thế divví dụ như nạp một tải TLB-miss giả mạo hoặc đắt tiền vào đường ống. Điều này không cần thiết sau cuộc retgọi tới hoặc gọi điện trực tiếp jmpở cuối một hàm.
Peter Cordes
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.