Những người liên kết làm gì?


127

Tôi luôn tự hỏi. Tôi biết rằng trình biên dịch chuyển đổi mã bạn viết thành mã nhị phân nhưng trình liên kết làm gì? Họ luôn là một bí ẩn đối với tôi.

Tôi đại khái hiểu 'liên kết' là gì. Đó là khi các tham chiếu đến thư viện và khuôn khổ được thêm vào hệ nhị phân. Tôi không hiểu gì ngoài điều đó. Đối với tôi nó "chỉ hoạt động". Tôi cũng hiểu những điều cơ bản về liên kết động nhưng không có gì quá sâu.

Ai đó có thể giải thích các điều khoản?

Câu trả lời:


160

Để hiểu trình liên kết, trước tiên cần hiểu điều gì xảy ra "ẩn" khi bạn chuyển đổi tệp nguồn (chẳng hạn như tệp C hoặc C ++) thành tệp thực thi (tệp thực thi là tệp có thể được thực thi trên máy của bạn hoặc máy của người khác đang chạy cùng một kiến ​​trúc máy).

Về cơ bản, khi một chương trình được biên dịch, trình biên dịch sẽ chuyển đổi tệp nguồn thành mã byte đối tượng. Mã byte này (đôi khi được gọi là mã đối tượng) là các hướng dẫn dễ nhớ mà chỉ kiến ​​trúc máy tính của bạn mới hiểu được. Theo truyền thống, các tệp này có phần mở rộng .OBJ.

Sau khi tệp đối tượng được tạo, trình liên kết sẽ hoạt động. Thường xuyên hơn không, một chương trình thực sự làm bất cứ điều gì hữu ích sẽ cần phải tham chiếu đến các tệp khác. Ví dụ, trong C, một chương trình đơn giản để in tên bạn ra màn hình sẽ bao gồm:

printf("Hello Kristina!\n");

Khi trình biên dịch biên dịch chương trình của bạn thành một tệp obj, nó chỉ cần đặt một tham chiếu đến printfhàm. Trình liên kết giải quyết tham chiếu này. Hầu hết các ngôn ngữ lập trình đều có một thư viện quy trình tiêu chuẩn để bao gồm những thứ cơ bản được mong đợi từ ngôn ngữ đó. Trình liên kết liên kết tệp OBJ của bạn với thư viện chuẩn này. Trình liên kết cũng có thể liên kết tệp OBJ của bạn với các tệp OBJ khác. Bạn có thể tạo các tệp OBJ khác có các hàm có thể được gọi bằng tệp OBJ khác. Trình liên kết hoạt động gần giống như sao chép và dán của trình xử lý văn bản. Nó "sao chép" tất cả các chức năng cần thiết mà chương trình của bạn tham chiếu và tạo ra một tệp thực thi duy nhất. Đôi khi các thư viện khác được sao chép ra ngoài phụ thuộc vào tệp OBJ hoặc thư viện khác. Đôi khi một trình liên kết phải đệ quy khá nhiều để thực hiện công việc của nó.

Lưu ý rằng không phải tất cả các hệ điều hành đều tạo một tệp thực thi duy nhất. Windows, chẳng hạn, sử dụng DLL để giữ tất cả các chức năng này cùng nhau trong một tệp duy nhất. Điều này làm giảm kích thước tệp thực thi của bạn, nhưng làm cho tệp thực thi của bạn phụ thuộc vào các DLL cụ thể này. DOS đã từng sử dụng những thứ được gọi là Lớp phủ (tệp .OVL). Điều này có nhiều mục đích, nhưng một mục đích là giữ các chức năng thường được sử dụng cùng nhau trong 1 tệp (một mục đích khác mà nó phục vụ, trong trường hợp bạn đang băn khoăn, là có thể đưa các chương trình lớn vào bộ nhớ. DOS có giới hạn về bộ nhớ và lớp phủ có thể được "dỡ bỏ" khỏi bộ nhớ và các lớp phủ khác có thể được "tải" lên trên bộ nhớ đó, do đó có tên, "lớp phủ"). Linux có các thư viện chia sẻ, về cơ bản có cùng ý tưởng với DLL (những người Linux lõi cứng mà tôi biết sẽ nói với tôi rằng có NHIỀU sự khác biệt LỚN).

Hy vọng điều này sẽ giúp bạn hiểu!


9
Câu trả lời chính xác. Ngoài ra, hầu hết các trình liên kết hiện đại sẽ loại bỏ mã dư thừa như phần trình bày mẫu.
Edward Strange

1
Đây có phải là một nơi thích hợp để xem xét một số khác biệt đó không?
John P

2
Xin chào, Giả sử tệp của tôi không tham chiếu bất kỳ tệp nào khác. Giả sử tôi chỉ cần khai báo và khởi tạo hai biến. Tệp nguồn này cũng sẽ chuyển đến trình liên kết chứ?
Mangesh Kherdekar

3
@MangeshKherdekar - Có, nó luôn đi qua một trình liên kết. Trình liên kết có thể không liên kết bất kỳ thư viện bên ngoài nào, nhưng giai đoạn liên kết vẫn phải xảy ra để tạo ra tệp thực thi.
Icemanind

78

Ví dụ tối thiểu về việc di dời địa chỉ

Chuyển địa chỉ là một trong những chức năng quan trọng của liên kết.

Vì vậy, hãy xem nó hoạt động như thế nào với một ví dụ tối thiểu.

0) Giới thiệu

Tóm tắt: việc tái định cư sẽ chỉnh sửa .textphần tệp đối tượng cần dịch:

  • địa chỉ tệp đối tượng
  • vào địa chỉ cuối cùng của tệp thực thi

Điều này phải được thực hiện bởi trình liên kết vì trình biên dịch chỉ xem một tệp đầu vào tại một thời điểm, nhưng chúng ta phải biết về tất cả các tệp đối tượng cùng một lúc để quyết định cách:

  • giải quyết các ký hiệu không xác định như các hàm không xác định đã khai báo
  • không đụng độ nhiều .textvà nhiều .dataphần của nhiều tệp đối tượng

Điều kiện tiên quyết: hiểu biết tối thiểu về:

Liên kết không liên quan gì đến C hoặc C ++ cụ thể: trình biên dịch chỉ tạo các tệp đối tượng. Sau đó, trình liên kết lấy chúng làm đầu vào mà không bao giờ biết ngôn ngữ nào đã biên dịch chúng. Nó cũng có thể là Fortran.

Vì vậy, để giảm bớt lớp vỏ, chúng ta hãy nghiên cứu một NASM x86-64 ELF Linux xin chào thế giới:

section .data
    hello_world db "Hello world!", 10
section .text
    global _start
    _start:

        ; sys_write
        mov rax, 1
        mov rdi, 1
        mov rsi, hello_world
        mov rdx, 13
        syscall

        ; sys_exit
        mov rax, 60
        mov rdi, 0
        syscall

được biên dịch và lắp ráp với:

nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o

với NASM 2.10.09.

1) .text của .o

Đầu tiên, chúng tôi dịch ngược .textphần của tệp đối tượng:

objdump -d hello_world.o

mang lại:

0000000000000000 <_start>:
   0:   b8 01 00 00 00          mov    $0x1,%eax
   5:   bf 01 00 00 00          mov    $0x1,%edi
   a:   48 be 00 00 00 00 00    movabs $0x0,%rsi
  11:   00 00 00
  14:   ba 0d 00 00 00          mov    $0xd,%edx
  19:   0f 05                   syscall
  1b:   b8 3c 00 00 00          mov    $0x3c,%eax
  20:   bf 00 00 00 00          mov    $0x0,%edi
  25:   0f 05                   syscall

những dòng quan trọng là:

   a:   48 be 00 00 00 00 00    movabs $0x0,%rsi
  11:   00 00 00

mà sẽ chuyển địa chỉ của chuỗi hello world vào thanh rsighi, địa chỉ này được chuyển đến lệnh gọi hệ thống ghi.

Nhưng đợi đã! Làm thế nào trình biên dịch có thể biết vị trí "Hello world!"sẽ kết thúc trong bộ nhớ khi chương trình được tải?

Chà, nó không thể, đặc biệt là sau khi chúng tôi liên kết một loạt các .otệp với nhiều .dataphần.

Chỉ trình liên kết mới có thể làm điều đó vì chỉ anh ta mới có tất cả các tệp đối tượng đó.

Vì vậy, trình biên dịch chỉ:

  • đặt giá trị giữ chỗ 0x0trên đầu ra đã biên dịch
  • cung cấp thêm một số thông tin cho trình liên kết về cách sửa đổi mã đã biên dịch với các địa chỉ tốt

"Thông tin bổ sung" này được chứa trong .rela.textphần của tệp đối tượng

2) .rela.text

.rela.text viết tắt của "chuyển vị trí của phần .text".

Từ chuyển vị trí được sử dụng vì trình liên kết sẽ phải chuyển địa chỉ từ đối tượng vào tệp thực thi.

Chúng tôi có thể tháo rời .rela.textphần bằng:

readelf -r hello_world.o

trong đó chứa;

Relocation section '.rela.text' at offset 0x340 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000c  000200000001 R_X86_64_64       0000000000000000 .data + 0

Định dạng của phần này được cố định thành tài liệu tại: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html

Mỗi mục nhập cho trình liên kết biết về một địa chỉ cần được di dời, ở đây chúng tôi chỉ có một địa chỉ cho chuỗi.

Đơn giản hóa một chút, đối với dòng cụ thể này, chúng tôi có thông tin sau:

  • Offset = C: byte đầu tiên của .textmục này thay đổi là gì.

    Nếu chúng ta nhìn lại văn bản đã dịch ngược, nó chính xác nằm bên trong lệnh quan trọng movabs $0x0,%rsivà những người biết mã hóa lệnh x86-64 sẽ nhận thấy rằng đoạn văn bản này mã hóa phần địa chỉ 64-bit của lệnh.

  • Name = .data: địa chỉ trỏ đến .dataphần

  • Type = R_X86_64_64, chỉ định chính xác những gì tính toán phải được thực hiện để dịch địa chỉ.

    Trường này thực sự phụ thuộc vào bộ xử lý, và do đó được ghi lại trên phần mở rộng AMD64 System V ABI 4.4 "Chuyển vị trí".

    Tài liệu đó nói rằng R_X86_64_64:

    • Field = word64: 8 byte, do đó 00 00 00 00 00 00 00 00địa chỉ tại0xC

    • Calculation = S + A

      • Sgiá trị tại địa chỉ được di dời, do đó00 00 00 00 00 00 00 00
      • Alà addend 0ở đây. Đây là một trường của mục nhập tái định cư.

      Vì vậy, S + A == 0và chúng tôi sẽ được chuyển đến địa chỉ đầu tiên của .dataphần.

3) .text của .out

Bây giờ, hãy xem vùng văn bản của tệp thực thi ldđược tạo cho chúng tôi:

objdump -d hello_world.out

cho:

00000000004000b0 <_start>:
  4000b0:   b8 01 00 00 00          mov    $0x1,%eax
  4000b5:   bf 01 00 00 00          mov    $0x1,%edi
  4000ba:   48 be d8 00 60 00 00    movabs $0x6000d8,%rsi
  4000c1:   00 00 00
  4000c4:   ba 0d 00 00 00          mov    $0xd,%edx
  4000c9:   0f 05                   syscall
  4000cb:   b8 3c 00 00 00          mov    $0x3c,%eax
  4000d0:   bf 00 00 00 00          mov    $0x0,%edi
  4000d5:   0f 05                   syscall

Vì vậy, điều duy nhất thay đổi từ tệp đối tượng là các dòng quan trọng:

  4000ba:   48 be d8 00 60 00 00    movabs $0x6000d8,%rsi
  4000c1:   00 00 00

mà bây giờ trỏ tới địa chỉ 0x6000d8( d8 00 60 00 00 00 00 00bằng little-endian) thay vì 0x0.

Đây có phải là vị trí thích hợp cho hello_worldchuỗi không?

Để quyết định, chúng ta phải kiểm tra các tiêu đề chương trình, tiêu đề này cho Linux biết nơi tải từng phần.

Chúng tôi tháo rời chúng bằng:

readelf -l hello_world.out

mang lại:

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000000d7 0x00000000000000d7  R E    200000
  LOAD           0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
                 0x000000000000000d 0x000000000000000d  RW     200000

 Section to Segment mapping:
  Segment Sections...
   00     .text
   01     .data

Điều này cho chúng ta biết rằng .dataphần, là phần thứ hai, bắt đầu tại VirtAddr= 0x06000d8.

Và thứ duy nhất trên phần dữ liệu là chuỗi hello world của chúng tôi.

Mức thưởng


1
Anh bạn thật tuyệt. Liên kết đến hướng dẫn 'cấu trúc chung của tệp ELF' bị hỏng.
Adam Zahran

1
@AdamZahran cảm ơn! Các URL trang GitHub ngu ngốc không thể đối phó với dấu gạch chéo!
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

15

Trong các ngôn ngữ như 'C', các mô-đun mã riêng lẻ theo truyền thống được biên dịch riêng biệt thành các đốm mã đối tượng, sẵn sàng thực thi ở mọi khía cạnh khác với tất cả các tham chiếu mà mô-đun tạo ra bên ngoài chính nó (tức là đến các thư viện hoặc các mô-đun khác) có vẫn chưa được giải quyết (tức là chúng đang trống, đang chờ ai đó đến và thực hiện tất cả các kết nối).

Những gì trình liên kết làm là xem xét tất cả các mô-đun cùng nhau, xem mỗi mô-đun cần gì để kết nối với bên ngoài chính nó và xem xét tất cả những thứ mà nó đang xuất. Sau đó, nó sửa chữa tất cả và tạo ra tệp thực thi cuối cùng, sau đó có thể chạy được.

Trong trường hợp liên kết động cũng đang diễn ra, đầu ra của trình liên kết vẫn không thể chạy - vẫn còn một số tham chiếu đến các thư viện bên ngoài chưa được giải quyết và chúng sẽ được HĐH giải quyết tại thời điểm nó tải ứng dụng (hoặc có thể thậm chí sau đó trong quá trình chạy).


Cần lưu ý rằng một số trình lắp ráp hoặc trình biên dịch có thể xuất ra tệp thực thi trực tiếp nếu trình biên dịch "thấy" mọi thứ cần thiết (thường trong một tệp nguồn duy nhất cộng với bất kỳ thứ gì mà nó #includes). Một số trình biên dịch, thường dành cho các trình biên dịch nhỏ, có chế độ hoạt động duy nhất.
supercat

Vâng, tôi đã cố gắng đưa ra câu trả lời giữa đường. Tất nhiên, cũng như trường hợp của bạn, điều ngược lại cũng đúng, trong đó một số loại tệp đối tượng thậm chí không có quá trình tạo mã đầy đủ; được thực hiện bởi trình liên kết (đó là cách tối ưu hóa toàn bộ chương trình MSVC hoạt động).
Will Dean vào

@WillDean và Tối ưu hóa thời gian liên kết của GCC, theo như tôi có thể nói - nó truyền trực tuyến tất cả 'mã' dưới dạng ngôn ngữ trung gian GIMPLE với siêu dữ liệu bắt buộc, cung cấp thông tin đó cho trình liên kết và tối ưu hóa trong một lần thực hiện ở cuối. (Bất chấp tài liệu đã lỗi thời ngụ ý gì, chỉ GIMPLE hiện được phát trực tuyến theo mặc định, thay vì chế độ 'chất béo' cũ với cả hai phần trình bày mã đối tượng.)
underscore_d

10

Khi trình biên dịch tạo ra một tệp đối tượng, nó bao gồm các mục nhập cho các ký hiệu được xác định trong tệp đối tượng đó và tham chiếu đến các ký hiệu không được xác định trong tệp đối tượng đó. Trình liên kết lấy những thứ đó và đặt chúng lại với nhau để (khi mọi thứ hoạt động bình thường) tất cả các tham chiếu bên ngoài từ mỗi tệp đều được thỏa mãn bởi các ký hiệu được xác định trong các tệp đối tượng khác.

Sau đó, nó kết hợp tất cả các tệp đối tượng đó lại với nhau và gán địa chỉ cho từng ký hiệu và khi một tệp đối tượng có tham chiếu bên ngoài đến tệp đối tượng khác, nó sẽ điền vào địa chỉ của mỗi ký hiệu ở bất cứ nơi nào mà đối tượng khác sử dụng. Trong trường hợp điển hình, nó cũng sẽ xây dựng một bảng gồm bất kỳ địa chỉ tuyệt đối nào được sử dụng, vì vậy trình tải có thể / sẽ "sửa chữa" các địa chỉ khi tệp được tải (tức là, nó sẽ thêm địa chỉ tải cơ sở vào từng địa chỉ đó địa chỉ để tất cả chúng đều tham chiếu đến địa chỉ bộ nhớ chính xác).

Khá nhiều trình liên kết hiện đại cũng có thể thực hiện một số (trong một số trường hợp là rất nhiều ) "công cụ" khác, chẳng hạn như tối ưu hóa mã theo những cách chỉ có thể thực hiện được khi tất cả các mô-đun đều hiển thị (ví dụ: xóa các chức năng đã được bao gồm bởi vì có thể một số mô-đun khác có thể gọi chúng, nhưng một khi tất cả các mô-đun được ghép lại với nhau thì rõ ràng là không có gì gọi chú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.