ลิงค์เกอร์ทำอะไร?


127

ฉันสงสัยมาตลอด ฉันรู้ว่าคอมไพเลอร์แปลงโค้ดที่คุณเขียนเป็นไบนารี แต่ลิงก์เกอร์จะทำอย่างไร พวกเขาเป็นปริศนาสำหรับฉันมาตลอด

ฉันเข้าใจคร่าวๆว่า 'การเชื่อมโยง' คืออะไร เมื่อมีการเพิ่มการอ้างอิงถึงไลบรารีและเฟรมเวิร์กลงในไบนารี ฉันไม่เข้าใจอะไรที่นอกเหนือไปจากนั้น สำหรับฉันมัน "ใช้ได้" ฉันยังเข้าใจพื้นฐานของการเชื่อมโยงแบบไดนามิก แต่ไม่มีอะไรลึกซึ้งเกินไป

ใครช่วยอธิบายเงื่อนไข

คำตอบ:


160

เพื่อทำความเข้าใจตัวเชื่อมโยงก่อนอื่นให้ทำความเข้าใจว่าเกิดอะไรขึ้น "ภายใต้ประทุน" เมื่อคุณแปลงไฟล์ต้นฉบับ (เช่นไฟล์ C หรือ C ++) เป็นไฟล์ปฏิบัติการ (ไฟล์ปฏิบัติการคือไฟล์ที่สามารถเรียกใช้งานบนเครื่องของคุณหรือ เครื่องของคนอื่นที่ใช้สถาปัตยกรรมเครื่องเดียวกัน)

ภายใต้ประทุนเมื่อมีการคอมไพล์โปรแกรมคอมไพลเลอร์จะแปลงไฟล์ต้นฉบับเป็นรหัสไบต์ของวัตถุ รหัสไบต์นี้ (บางครั้งเรียกว่ารหัสวัตถุ) เป็นคำสั่งช่วยในการจำที่มีเพียงสถาปัตยกรรมคอมพิวเตอร์ของคุณเท่านั้นที่เข้าใจ ตามเนื้อผ้าไฟล์เหล่านี้มีนามสกุล. OBJ

หลังจากสร้างไฟล์ออบเจ็กต์แล้วตัวเชื่อมโยงจะเข้ามาเล่น บ่อยกว่านั้นโปรแกรมจริงที่ทำอะไรก็ได้ที่เป็นประโยชน์จะต้องอ้างอิงไฟล์อื่น ๆ ตัวอย่างเช่นใน C โปรแกรมง่ายๆในการพิมพ์ชื่อของคุณบนหน้าจอจะประกอบด้วย:

printf("Hello Kristina!\n");

เมื่อคอมไพเลอร์คอมไพเลอร์โปรแกรมของคุณเป็นไฟล์ obj มันก็จะอ้างอิงถึงprintfฟังก์ชัน ตัวเชื่อมโยงแก้ไขข้อมูลอ้างอิงนี้ ภาษาโปรแกรมส่วนใหญ่มีไลบรารีรูทีนมาตรฐานเพื่อครอบคลุมสิ่งพื้นฐานที่คาดหวังจากภาษานั้น ตัวเชื่อมโยงไฟล์ OBJ ของคุณกับไลบรารีมาตรฐานนี้ ตัวเชื่อมโยงยังสามารถเชื่อมโยงไฟล์ OBJ ของคุณกับไฟล์ OBJ อื่น ๆ คุณสามารถสร้างไฟล์ OBJ อื่นที่มีฟังก์ชันที่สามารถเรียกโดยไฟล์ OBJ อื่นได้ ตัวเชื่อมโยงทำงานเกือบเหมือนกับสำเนาและวางของโปรแกรมประมวลผลคำ มัน "คัดลอก" ฟังก์ชันที่จำเป็นทั้งหมดที่โปรแกรมของคุณอ้างอิงและสร้างไฟล์ปฏิบัติการเดียว บางครั้งไลบรารีอื่น ๆ ที่คัดลอกออกมานั้นขึ้นอยู่กับ OBJ หรือไฟล์ไลบรารีอื่น ๆ บางครั้งผู้เชื่อมโยงต้องได้รับการเรียกซ้ำเพื่อทำงานของมัน

โปรดทราบว่าระบบปฏิบัติการบางระบบไม่ได้สร้างไฟล์ปฏิบัติการเดียว ตัวอย่างเช่น Windows ใช้ DLL ที่รวบรวมฟังก์ชันเหล่านี้ทั้งหมดไว้ด้วยกันในไฟล์เดียว ซึ่งจะช่วยลดขนาดไฟล์ปฏิบัติการของคุณ แต่ทำให้ไฟล์ปฏิบัติการของคุณขึ้นอยู่กับ DLL เฉพาะเหล่านี้ DOS เคยใช้สิ่งที่เรียกว่า Overlays (ไฟล์. OVL) สิ่งนี้มีจุดประสงค์หลายประการ แต่อย่างหนึ่งคือการเก็บฟังก์ชันที่ใช้กันทั่วไปไว้ด้วยกันใน 1 ไฟล์ (อีกจุดประสงค์หนึ่งที่ใช้ในกรณีที่คุณสงสัยคือสามารถใส่โปรแกรมขนาดใหญ่ลงในหน่วยความจำได้ DOS มีข้อ จำกัด ในหน่วยความจำและการซ้อนทับสามารถทำได้ ถูก "ยกเลิกการโหลด" จากหน่วยความจำและภาพซ้อนทับอื่น ๆ สามารถ "โหลด" ที่ด้านบนของหน่วยความจำนั้นได้จึงเรียกชื่อ "โอเวอร์เลย์") Linux มีไลบรารีที่ใช้ร่วมกันซึ่งโดยพื้นฐานแล้วเป็นแนวคิดเดียวกับ DLL (พวก Linux ฮาร์ดคอร์ที่ฉันรู้ว่าจะบอกฉันว่ามีความแตกต่างมากมาย)

หวังว่านี่จะช่วยให้คุณเข้าใจ!


9
คำตอบที่ดี นอกจากนี้ตัวเชื่อมโยงสมัยใหม่ส่วนใหญ่จะลบโค้ดที่ซ้ำซ้อนเช่นการสร้างอินสแตนซ์เทมเพลต
Edward Strange

1
นี่เป็นสถานที่ที่เหมาะสมในการก้าวข้ามความแตกต่างเหล่านั้นหรือไม่?
John P

2
สวัสดีสมมติว่าไฟล์ของฉันไม่ได้อ้างอิงไฟล์อื่นใด สมมติว่าฉันเพิ่งประกาศและเริ่มต้นตัวแปรสองตัว ซอร์สไฟล์นี้จะไปที่ตัวเชื่อมโยงด้วยหรือไม่
Mangesh Kherdekar

3
@MangeshKherdekar - ใช่มันจะต้องผ่านตัวเชื่อมโยงเสมอ ตัวเชื่อมโยงอาจไม่เชื่อมโยงไลบรารีภายนอกใด ๆ แต่เฟสการเชื่อมโยงยังคงต้องเกิดขึ้นเพื่อสร้างไฟล์ปฏิบัติการ
Icemanind

78

ตัวอย่างการย้ายที่อยู่ขั้นต่ำ

การย้ายที่อยู่เป็นหน้าที่สำคัญอย่างหนึ่งของการเชื่อมโยง

ลองมาดูวิธีการทำงานกับตัวอย่างขั้นต่ำ

0) บทนำ

สรุป: การย้ายตำแหน่งแก้ไข.textส่วนของไฟล์ออบเจ็กต์ที่จะแปล:

  • ที่อยู่ไฟล์ออบเจ็กต์
  • ลงในที่อยู่สุดท้ายของไฟล์ปฏิบัติการ

สิ่งนี้ต้องทำโดยตัวเชื่อมโยงเนื่องจากคอมไพเลอร์เห็นไฟล์อินพุตทีละไฟล์เท่านั้น แต่เราต้องรู้เกี่ยวกับไฟล์อ็อบเจ็กต์ทั้งหมดพร้อมกันเพื่อตัดสินใจว่าจะทำอย่างไร:

  • แก้ไขสัญลักษณ์ที่ไม่ได้กำหนดเช่นฟังก์ชันที่ไม่ได้กำหนดที่ประกาศไว้
  • ไม่ปะทะกันหลายส่วน.textและ.dataส่วนของไฟล์อ็อบเจ็กต์หลายไฟล์

วิชาบังคับก่อน: มีความเข้าใจน้อยที่สุดเกี่ยวกับ:

การเชื่อมโยงไม่มีส่วนเกี่ยวข้องกับ C หรือ C ++ โดยเฉพาะ: คอมไพเลอร์สร้างไฟล์อ็อบเจ็กต์ จากนั้นตัวเชื่อมโยงจะนำข้อมูลเหล่านั้นไปเป็นอินพุตโดยไม่ทราบว่าภาษาใดรวบรวม มันอาจเป็น Fortran เช่นกัน

ดังนั้นเพื่อลดเปลือกลองศึกษา NASM x86-64 ELF Linux สวัสดีชาวโลก:

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

รวบรวมและประกอบด้วย:

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

ด้วย NASM 2.10.09

1). ข้อความของ. o

ก่อนอื่นเราแยก.textส่วนของไฟล์ออบเจ็กต์:

objdump -d hello_world.o

ซึ่งจะช่วยให้:

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

บรรทัดสำคัญคือ:

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

ซึ่งควรย้ายที่อยู่ของสตริง hello world ไปไว้ในไฟล์ rsiรีจิสเตอร์ซึ่งจะส่งผ่านไปยังการเรียกระบบการเขียน

แต่เดี๋ยวก่อน! คอมไพเลอร์จะรู้ได้อย่างไรว่าอยู่ที่ไหน"Hello world!"จะสิ้นสุดลงในหน่วยความจำเมื่อโหลดโปรแกรม?

มันทำไม่ได้โดยเฉพาะหลังจากที่เราเชื่อมโยง.oไฟล์หลาย ๆ ไฟล์เข้าด้วยกัน.dataส่วน

มีเพียงผู้เชื่อมโยงเท่านั้นที่สามารถทำได้เนื่องจากมีเพียงไฟล์วัตถุเหล่านั้นเท่านั้นที่จะมี

ดังนั้นคอมไพเลอร์เพียง:

  • ใส่ค่าตัวยึดตำแหน่ง 0x0บนเอาต์พุตที่คอมไพล์แล้ว
  • ให้ข้อมูลเพิ่มเติมแก่ผู้เชื่อมโยงเกี่ยวกับวิธีแก้ไขโค้ดที่คอมไพล์แล้วด้วยที่อยู่ที่ดี

"ข้อมูลเพิ่มเติม" นี้มีอยู่ในไฟล์ .rela.textส่วนของออบเจ็กต์ไฟล์

2) .rela.text

.rela.text ย่อมาจาก "การย้ายส่วน. text"

ใช้คำว่า relocation เนื่องจากตัวเชื่อมโยงจะต้องย้ายที่อยู่จากวัตถุไปยังไฟล์ปฏิบัติการ

เราสามารถแยกชิ้น.rela.textส่วนได้ด้วย:

readelf -r hello_world.o

ซึ่งประกอบด้วย;

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

รูปแบบของส่วนนี้ได้รับการแก้ไขแล้วที่: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html

แต่ละรายการจะบอกผู้เชื่อมโยงเกี่ยวกับที่อยู่หนึ่งที่ต้องย้ายที่นี่เรามีเพียงที่อยู่เดียวสำหรับสตริง

ทำให้ง่ายขึ้นเล็กน้อยสำหรับบรรทัดนี้เรามีข้อมูลต่อไปนี้:

  • Offset = C: ไบต์แรกของการ.textเปลี่ยนแปลงนี้คืออะไร

    หากเรามองย้อนกลับไปที่ข้อความที่ถอดรหัสแล้วข้อความนั้นอยู่ในขั้นวิกฤตmovabs $0x0,%rsiและผู้ที่รู้การเข้ารหัสคำสั่ง x86-64 จะสังเกตว่าสิ่งนี้เข้ารหัสส่วนที่อยู่ 64 บิตของคำสั่ง

  • Name = .data: ที่อยู่ชี้ไปที่.dataส่วน

  • Type = R_X86_64_64ซึ่งระบุสิ่งที่ต้องคำนวณเพื่อแปลที่อยู่

    ฟิลด์นี้ขึ้นอยู่กับโปรเซสเซอร์จริงๆดังนั้นจึงมีการบันทึกไว้ในส่วนขยาย AMD64 System V ABIส่วน 4.4 "การย้ายตำแหน่ง"

    เอกสารนั้นระบุว่าR_X86_64_64:

    • Field = word64: 8 ไบต์ตาม00 00 00 00 00 00 00 00ที่อยู่0xC

    • Calculation = S + A

      • Sคือค่าตามที่อยู่ที่ถูกย้ายดังนั้น00 00 00 00 00 00 00 00
      • Aคือส่วนเสริมที่อยู่0ที่นี่ นี่คือฟิลด์ของรายการการย้ายที่ตั้ง

      ดังนั้นS + A == 0เราจะได้รับการย้ายไปยังที่อยู่เป็นครั้งแรกของการ.dataแสดง

3) .text ของ. out

ตอนนี้ให้ดูที่พื้นที่ข้อความของไฟล์ปฏิบัติการที่ldสร้างขึ้นสำหรับเรา:

objdump -d hello_world.out

ให้:

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

ดังนั้นสิ่งเดียวที่เปลี่ยนไปจากไฟล์ออบเจ็กต์คือเส้นวิกฤต:

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

ซึ่งขณะนี้ชี้ไปที่ที่อยู่0x6000d8( d8 00 60 00 00 00 00 00ในน้อย endian) 0x0แทน

นี่คือตำแหน่งที่เหมาะสมสำหรับไฟล์ hello_worldสตริงหรือไม่

ในการตัดสินใจเราต้องตรวจสอบส่วนหัวของโปรแกรมซึ่งบอก Linux ว่าจะโหลดแต่ละส่วนไปที่ใด

เราแยกชิ้นส่วนด้วย:

readelf -l hello_world.out

ซึ่งจะช่วยให้:

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

นี้จะบอกเราว่า.dataส่วนซึ่งเป็นคนที่สองเริ่มต้นที่=VirtAddr0x06000d8

และสิ่งเดียวในส่วนข้อมูลคือสตริงสวัสดีชาวโลกของเรา

ระดับโบนัส


1
คุณสุดยอดมาก ลิงก์ไปยังบทช่วยสอน "โครงสร้างส่วนกลางของไฟล์ ELF" เสีย
Adam Zahran

1
@AdamZahran ขอบคุณ! URL หน้า GitHub โง่ที่ไม่สามารถจัดการกับเครื่องหมายทับได้!
Ciro Santilli 郝海东冠状病六四事件法轮功

15

ในภาษาเช่น 'C' แต่ละโมดูลของโค้ดจะถูกคอมไพล์แยกกันเป็นกลุ่มของโค้ดอ็อบเจ็กต์ซึ่งพร้อมที่จะดำเนินการในทุก ๆ ด้านนอกเหนือจากการอ้างอิงทั้งหมดที่โมดูลสร้างขึ้นภายนอกตัวเอง (เช่นไลบรารีหรือโมดูลอื่น ๆ ) มี ยังไม่ได้รับการแก้ไข (กล่าวคือว่างอยู่รอให้มีคนมาหาและทำการเชื่อมต่อทั้งหมด)

สิ่งที่ผู้เชื่อมโยงทำคือการดูโมดูลทั้งหมดเข้าด้วยกันดูว่าแต่ละโมดูลต้องเชื่อมต่อกับภายนอกอย่างไรและดูสิ่งที่ส่งออกทั้งหมด จากนั้นแก้ไขทั้งหมดและสร้างไฟล์ปฏิบัติการขั้นสุดท้ายซึ่งสามารถรันได้

ในกรณีที่การเชื่อมโยงแบบไดนามิกเกิดขึ้นเอาต์พุตของตัวเชื่อมโยงยังไม่สามารถเรียกใช้งานได้ - ยังมีการอ้างอิงถึงไลบรารีภายนอกบางส่วนที่ยังไม่ได้รับการแก้ไขและระบบปฏิบัติการจะได้รับการแก้ไขในขณะที่โหลดแอป (หรืออาจ แม้ในภายหลังระหว่างการวิ่ง)


เป็นที่น่าสังเกตว่าแอสเซมเบลอร์หรือคอมไพเลอร์บางตัวสามารถส่งออกไฟล์ปฏิบัติการได้โดยตรงหากคอมไพเลอร์ "เห็น" ทุกสิ่งที่จำเป็น (โดยทั่วไปจะอยู่ในซอร์สไฟล์เดียวรวมถึงอะไรก็ตามที่ # รวมอยู่ด้วย) คอมไพเลอร์สองสามตัวโดยทั่วไปสำหรับไมโครขนาดเล็กมีสิ่งนั้นเป็นโหมดการทำงานเดียว
supercat

ใช่ฉันพยายามให้คำตอบกลางถนน แน่นอนเช่นเดียวกับกรณีของคุณตรงกันข้ามก็เป็นจริงเช่นกันเนื่องจากอ็อบเจ็กต์ไฟล์บางชนิดไม่มีการสร้างโค้ดแบบเต็ม ที่ทำโดยตัวเชื่อมโยง (นั่นคือวิธีการทำงานของ MSVC ทั้งโปรแกรมเพิ่มประสิทธิภาพ)
Will Dean

@ WillDean และ GCC's Link-Time Optimization เท่าที่ฉันสามารถบอกได้ - มันสตรีม 'รหัส' ทั้งหมดเป็นภาษากลาง GIMPLE พร้อมด้วยข้อมูลเมตาที่จำเป็นทำให้สามารถใช้งานได้กับตัวเชื่อมโยงและปรับให้เหมาะสมในครั้งเดียวในตอนท้าย (แม้จะมีความหมายถึงเอกสารที่ล้าสมัย แต่ตอนนี้ GIMPLE เท่านั้นที่สตรีมโดยค่าเริ่มต้นแทนที่จะเป็นโหมด 'fat' แบบเก่าที่มีการแสดงรหัสอ็อบเจ็กต์ทั้งคู่)
underscore_d

10

เมื่อคอมไพลเลอร์สร้างอ็อบเจ็กต์ไฟล์คอมไพเลอร์จะรวมรายการสำหรับสัญลักษณ์ที่กำหนดไว้ในอ็อบเจ็กต์ไฟล์นั้นและการอ้างอิงถึงสัญลักษณ์ที่ไม่ได้กำหนดไว้ในอ็อบเจ็กต์ไฟล์นั้น ตัวเชื่อมโยงจะนำสิ่งเหล่านี้มารวมเข้าด้วยกัน (เมื่อทุกอย่างทำงานได้ถูกต้อง) การอ้างอิงภายนอกทั้งหมดจากแต่ละไฟล์จะได้รับความพึงพอใจจากสัญลักษณ์ที่กำหนดไว้ในอ็อบเจ็กต์ไฟล์อื่น ๆ

จากนั้นจะรวมไฟล์อ็อบเจ็กต์เหล่านั้นทั้งหมดเข้าด้วยกันและกำหนดแอดเดรสให้กับแต่ละสัญลักษณ์และโดยที่อ็อบเจ็กต์ไฟล์หนึ่งมีการอ้างอิงภายนอกไปยังอ็อบเจ็กต์ไฟล์อื่นไฟล์นั้นจะเติมที่อยู่ของแต่ละสัญลักษณ์ทุกที่ที่อ็อบเจ็กต์อื่นใช้ ในกรณีทั่วไปมันจะสร้างตารางของที่อยู่สัมบูรณ์ที่ใช้ดังนั้นตัวโหลดสามารถ / จะ "แก้ไข" ที่อยู่เมื่อโหลดไฟล์ (กล่าวคือจะเพิ่มที่อยู่โหลดพื้นฐานให้กับแต่ละที่อยู่ ที่อยู่ทั้งหมดจึงอ้างถึงที่อยู่หน่วยความจำที่ถูกต้อง)

ตัวเชื่อมโยงสมัยใหม่จำนวนไม่น้อยยังสามารถดำเนินการ"สิ่งต่างๆ" อื่น ๆ ได้(ในบางกรณีมาก ) เช่นการเพิ่มประสิทธิภาพโค้ดในรูปแบบที่เป็นไปได้ก็ต่อเมื่อโมดูลทั้งหมดสามารถมองเห็นได้ (เช่นการลบฟังก์ชันที่รวมอยู่ด้วย เนื่องจากเป็นไปได้ว่าโมดูลอื่น ๆ อาจเรียกโมดูลเหล่านี้ แต่เมื่อรวมโมดูลทั้งหมดเข้าด้วยกันแล้วจะเห็นได้ชัดว่าไม่มีอะไรเรียกใช้)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.