Quá trình biên dịch / liên kết hoạt động như thế nào?


416

Quá trình biên dịch và liên kết hoạt động như thế nào?

(Lưu ý: Đây có nghĩa là một mục trong Câu hỏi thường gặp về C ++ của Stack Overflow . Nếu bạn muốn phê bình ý tưởng cung cấp Câu hỏi thường gặp trong biểu mẫu này, thì bài đăng trên meta bắt đầu tất cả điều này sẽ là nơi để thực hiện điều đó. câu hỏi đó được theo dõi trong phòng chat C ++ , nơi ý tưởng FAQ bắt đầu ngay từ đầu, vì vậy câu trả lời của bạn rất có thể được đọc bởi những người nghĩ ra ý tưởng.)

Câu trả lời:


554

Việc biên dịch chương trình C ++ bao gồm ba bước:

  1. Tiền xử lý: bộ tiền xử lý lấy tệp mã nguồn C ++ và xử lý các chỉ thị #includes, #defines và các bộ xử lý tiền xử lý khác. Đầu ra của bước này là tệp C ++ "thuần túy" không có chỉ thị tiền xử lý.

  2. Biên dịch: trình biên dịch lấy đầu ra của bộ xử lý trước và tạo một tệp đối tượng từ nó.

  3. Liên kết: trình liên kết lấy các tệp đối tượng được tạo bởi trình biên dịch và tạo thư viện hoặc tệp thực thi.

Sơ chế

Bộ tiền xử lý xử lý các chỉ thị tiền xử lý , như #include#define. Không rõ về cú pháp của C ++, đó là lý do tại sao nó phải được sử dụng cẩn thận.

Nó hoạt động trên một file C ++ nguồn tại một thời điểm bằng cách thay thế #includechỉ thị với nội dung của các tập tin tương ứng (mà thường chỉ là tờ khai), làm thay macro ( #define), và chọn các phần khác nhau của văn bản phụ thuộc vào #if, #ifdef#ifndefchỉ thị.

Bộ tiền xử lý hoạt động trên luồng mã thông báo tiền xử lý. Thay thế macro được định nghĩa là thay thế các mã thông báo bằng các mã thông báo khác (toán tử ##cho phép hợp nhất hai mã thông báo khi có ý nghĩa).

Sau tất cả điều này, bộ tiền xử lý tạo ra một đầu ra duy nhất là một luồng mã thông báo do các biến đổi được mô tả ở trên. Nó cũng thêm một số dấu hiệu đặc biệt cho trình biên dịch biết mỗi dòng đến từ đâu để nó có thể sử dụng các dấu này để tạo ra các thông báo lỗi hợp lý.

Một số lỗi có thể được tạo ra ở giai đoạn này với việc sử dụng thông minh các chỉ thị #if#errorchỉ thị.

Biên soạn

Bước biên dịch được thực hiện trên mỗi đầu ra của bộ tiền xử lý. Trình biên dịch phân tích mã nguồn C ++ thuần túy (bây giờ không có bất kỳ chỉ thị tiền xử lý nào) và chuyển đổi nó thành mã lắp ráp. Sau đó gọi back-end bên dưới (trình biên dịch trong chuỗi công cụ) để lắp mã đó thành mã máy tạo ra tệp nhị phân thực tế ở một số định dạng (ELF, COFF, a.out, ...). Tệp đối tượng này chứa mã được biên dịch (ở dạng nhị phân) của các ký hiệu được xác định trong đầu vào. Biểu tượng trong các tệp đối tượng được gọi bằng tên.

Các tệp đối tượng có thể tham chiếu đến các ký hiệu không được xác định. Đây là trường hợp khi bạn sử dụng khai báo và không cung cấp định nghĩa cho nó. Trình biên dịch không quan tâm đến điều này và sẽ vui vẻ tạo ra tệp đối tượng miễn là mã nguồn được định dạng tốt.

Trình biên dịch thường cho phép bạn dừng biên dịch vào thời điểm này. Điều này rất hữu ích vì với nó, bạn có thể biên dịch từng tệp mã nguồn riêng biệt. Ưu điểm này cung cấp là bạn không cần biên dịch lại mọi thứ nếu bạn chỉ thay đổi một tệp duy nhất.

Các tệp đối tượng được sản xuất có thể được đặt trong kho lưu trữ đặc biệt gọi là thư viện tĩnh, để dễ dàng sử dụng lại sau này.

Ở giai đoạn này, các lỗi trình biên dịch "thông thường", như lỗi cú pháp hoặc lỗi giải quyết quá tải không thành công, được báo cáo.

Liên kết

Trình liên kết là thứ tạo ra đầu ra biên dịch cuối cùng từ các tệp đối tượng mà trình biên dịch tạo ra. Đầu ra này có thể là thư viện dùng chung (hoặc động) (và mặc dù tên tương tự, nhưng chúng không có nhiều điểm chung với các thư viện tĩnh được đề cập trước đó) hoặc có thể thực thi được.

Nó liên kết tất cả các tệp đối tượng bằng cách thay thế các tham chiếu đến các ký hiệu không xác định bằng các địa chỉ chính xác. Mỗi ký hiệu này có thể được định nghĩa trong các tệp đối tượng khác hoặc trong các thư viện. Nếu chúng được định nghĩa trong các thư viện khác với thư viện chuẩn, bạn cần nói với người liên kết về chúng.

Ở giai đoạn này, các lỗi phổ biến nhất là thiếu định nghĩa hoặc định nghĩa trùng lặp. Điều này có nghĩa là các định nghĩa không tồn tại (nghĩa là chúng không được viết) hoặc các tệp đối tượng hoặc thư viện nơi chúng cư trú không được cung cấp cho trình liên kết. Điều thứ hai là rõ ràng: cùng một biểu tượng được xác định trong hai tệp đối tượng hoặc thư viện khác nhau.


39
Giai đoạn biên dịch cũng gọi trình biên dịch chương trình trước khi chuyển đổi thành tệp đối tượng.
manav mn

3
Tối ưu hóa được áp dụng ở đâu? Thoạt nhìn có vẻ như nó sẽ được thực hiện trong bước biên dịch, nhưng mặt khác tôi có thể tưởng tượng rằng việc tối ưu hóa phù hợp chỉ có thể được thực hiện sau khi liên kết.
Bart van Heukelom

6
@BartvanHeukelom theo truyền thống được thực hiện trong quá trình biên dịch, nhưng các trình biên dịch hiện đại hỗ trợ cái gọi là "tối ưu hóa thời gian liên kết" có lợi thế là có thể tối ưu hóa trên các đơn vị dịch thuật.
R. Martinho Fernandes

3
C có các bước tương tự không?
Kevin Zhu

6
Nếu trình liên kết chuyển đổi các ký hiệu tham chiếu đến các lớp / phương thức trong các thư viện thành các địa chỉ, điều đó có nghĩa là các nhị phân thư viện được lưu trữ trong các địa chỉ bộ nhớ mà HĐH giữ không đổi? Tôi chỉ bối rối về cách trình liên kết sẽ biết địa chỉ chính xác của nhị phân stdio cho tất cả các hệ thống đích. Đường dẫn tệp sẽ luôn giống nhau, nhưng địa chỉ chính xác có thể thay đổi, phải không?
Dan Carter

42

Chủ đề này được thảo luận tại CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html

Đây là những gì tác giả ở đó đã viết:

Biên dịch không hoàn toàn giống như tạo một tệp thực thi! Thay vào đó, tạo một tệp thực thi là một quá trình nhiều tầng được chia thành hai thành phần: biên dịch và liên kết. Trong thực tế, ngay cả khi một chương trình "biên dịch tốt", nó có thể không thực sự hoạt động do lỗi trong giai đoạn liên kết. Toàn bộ quá trình đi từ các tệp mã nguồn đến một tệp thực thi có thể được gọi là một bản dựng.

Biên soạn

Quá trình biên dịch đề cập đến việc xử lý các tệp mã nguồn (.c, .cc hoặc .cpp) và tạo tệp 'đối tượng'. Bước này không tạo ra bất cứ thứ gì mà người dùng thực sự có thể chạy. Thay vào đó, trình biên dịch chỉ tạo ra các hướng dẫn ngôn ngữ máy tương ứng với tệp mã nguồn đã được biên dịch. Chẳng hạn, nếu bạn biên dịch (nhưng không liên kết) ba tệp riêng biệt, bạn sẽ có ba tệp đối tượng được tạo làm đầu ra, mỗi tệp có tên .o hoặc .obj (phần mở rộng sẽ phụ thuộc vào trình biên dịch của bạn). Mỗi tệp này chứa một bản dịch tệp mã nguồn của bạn thành tệp ngôn ngữ máy - nhưng bạn chưa thể chạy chúng! Bạn cần biến chúng thành các tệp thực thi mà hệ điều hành của bạn có thể sử dụng. Đó là nơi liên kết đến.

Liên kết

Liên kết đề cập đến việc tạo ra một tệp thực thi duy nhất từ ​​nhiều tệp đối tượng. Trong bước này, thông thường là trình liên kết sẽ phàn nàn về các hàm không xác định (thông thường, chính nó). Trong quá trình biên dịch, nếu trình biên dịch không thể tìm thấy định nghĩa cho một hàm cụ thể, thì nó sẽ chỉ cho rằng hàm đó được định nghĩa trong một tệp khác. Nếu đây không phải là trường hợp, thì không có cách nào trình biên dịch sẽ biết - nó không xem xét nội dung của nhiều tệp cùng một lúc. Mặt khác, trình liên kết có thể xem nhiều tệp và cố gắng tìm các tham chiếu cho các chức năng không được đề cập.

Bạn có thể hỏi tại sao có các bước biên dịch và liên kết riêng biệt. Đầu tiên, có lẽ dễ dàng hơn để thực hiện mọi thứ theo cách đó. Trình biên dịch thực hiện công việc của nó và trình liên kết thực hiện công việc của nó - bằng cách giữ các chức năng riêng biệt, độ phức tạp của chương trình giảm đi. Một ưu điểm khác (rõ ràng hơn) là điều này cho phép tạo ra các chương trình lớn mà không phải làm lại bước biên dịch mỗi khi thay đổi tệp. Thay vào đó, bằng cách sử dụng cái gọi là "biên dịch có điều kiện", chỉ cần biên dịch những tệp nguồn đã thay đổi; đối với phần còn lại, các tệp đối tượng là đầu vào đủ cho trình liên kết. Cuối cùng, điều này làm cho việc triển khai các thư viện mã được biên dịch trước trở nên đơn giản: chỉ cần tạo các tệp đối tượng và liên kết chúng giống như bất kỳ tệp đối tượng nào khác.

Để có được lợi ích đầy đủ của việc biên dịch điều kiện, có thể dễ dàng có được một chương trình giúp bạn hơn là thử và nhớ những tệp bạn đã thay đổi kể từ lần biên dịch cuối cùng. (Tất nhiên, bạn có thể biên dịch lại mọi tệp có dấu thời gian lớn hơn dấu thời gian của tệp đối tượng tương ứng.) Nếu bạn đang làm việc với môi trường phát triển tích hợp (IDE), nó có thể xử lý việc này cho bạn. Nếu bạn đang sử dụng các công cụ dòng lệnh, sẽ có một tiện ích tiện lợi được gọi là make đi kèm với hầu hết các bản phân phối * nix. Cùng với việc biên dịch có điều kiện, nó có một số tính năng hay khác để lập trình, chẳng hạn như cho phép các phần biên dịch khác nhau của chương trình của bạn - ví dụ, nếu bạn có một phiên bản tạo đầu ra dài dòng để gỡ lỗi.

Biết được sự khác biệt giữa giai đoạn biên dịch và giai đoạn liên kết có thể giúp việc săn bọ dễ dàng hơn. Lỗi trình biên dịch thường là cú pháp trong tự nhiên - một dấu chấm phẩy bị thiếu, dấu ngoặc đơn phụ. Lỗi liên kết thường phải làm với thiếu hoặc nhiều định nghĩa. Nếu bạn gặp lỗi rằng một hàm hoặc biến được xác định nhiều lần từ trình liên kết, thì đó là một dấu hiệu tốt cho thấy lỗi đó là hai tệp mã nguồn của bạn có cùng chức năng hoặc biến.


1
Điều tôi không hiểu là nếu bộ tiền xử lý quản lý những thứ như #includes để tạo một siêu tệp thì chắc chắn không có gì để liên kết sau đó?
binarysmacker

@binarysmacer Xem những gì tôi viết dưới đây có ý nghĩa gì với bạn không. Tôi đã cố gắng để mô tả vấn đề từ trong ra ngoài.
Chế độ xem hình elip

3
@binarysmacker Đã quá muộn để bình luận về điều này, nhưng những người khác có thể thấy điều này hữu ích. youtu.be/D0TazQIkc8Q Về cơ bản, bạn bao gồm các tệp tiêu đề và các tệp tiêu đề này thường chỉ chứa các khai báo biến / hàm và không có định nghĩa, định nghĩa có thể có trong một tệp nguồn riêng biệt. Vì vậy, bộ xử lý chỉ bao gồm các khai báo và không phải là định nghĩa. linker help. Bạn liên kết tệp nguồn sử dụng biến / hàm với tệp nguồn xác định chúng.
Karan Joower

24

Trên mặt trận tiêu chuẩn:

  • một đơn vị dịch là sự kết hợp của một tệp nguồn, bao gồm các tiêu đề và các tệp nguồn ít hơn bất kỳ dòng nguồn nào bị bỏ qua bởi chỉ thị tiền xử lý bao gồm có điều kiện.

  • tiêu chuẩn xác định 9 giai đoạn trong bản dịch. Bốn phần đầu tương ứng với tiền xử lý, ba phần tiếp theo là phần tổng hợp, phần tiếp theo là phần khởi tạo của các mẫu ( tạo các đơn vị khởi tạo ) và phần cuối là phần liên kết.

Trong thực tế, giai đoạn thứ tám (khởi tạo các mẫu) thường được thực hiện trong quá trình biên dịch nhưng một số trình biên dịch trì hoãn nó sang giai đoạn liên kết và một số lan truyền nó trong hai.


14
Bạn có thể liệt kê tất cả 9 giai đoạn? Đó là một bổ sung tốt đẹp cho câu trả lời, tôi nghĩ vậy. :)
jalf


@jalf, chỉ cần thêm phần khởi tạo mẫu ngay trước pha cuối cùng trong câu trả lời được chỉ ra bởi @sbi. IIRC có sự khác biệt tinh tế trong cách diễn đạt chính xác trong việc xử lý các ký tự rộng, nhưng tôi không nghĩ rằng chúng xuất hiện trong nhãn sơ đồ.
AProgrammer

2
@sbi yeah, nhưng đây được cho là câu hỏi thường gặp phải không? Vì vậy, thông tin này không có sẵn ở đây ? ;)
jalf

3
@AProgrammmer: chỉ cần liệt kê chúng theo tên sẽ hữu ích. Sau đó mọi người biết những gì cần tìm kiếm nếu họ muốn biết thêm chi tiết. Dù sao, + 1'ed câu trả lời của bạn trong mọi trường hợp :)
jalf

14

Điểm yếu là CPU tải dữ liệu từ các địa chỉ bộ nhớ, lưu trữ dữ liệu vào địa chỉ bộ nhớ và thực hiện các lệnh liên tục ra khỏi địa chỉ bộ nhớ, với một số bước nhảy có điều kiện trong chuỗi các lệnh được xử lý. Mỗi trong ba loại hướng dẫn này liên quan đến việc tính toán một địa chỉ cho một ô nhớ được sử dụng trong hướng dẫn máy. Bởi vì các hướng dẫn máy có độ dài thay đổi tùy thuộc vào hướng dẫn cụ thể có liên quan và do chúng tôi kết hợp độ dài biến đổi của chúng với nhau khi chúng tôi xây dựng mã máy, nên có một quy trình gồm hai bước liên quan đến tính toán và xây dựng bất kỳ địa chỉ nào.

Đầu tiên chúng ta đặt ra sự phân bổ bộ nhớ tốt nhất có thể trước khi chúng ta có thể biết chính xác những gì diễn ra trong mỗi ô. Chúng tôi tìm ra các byte, hoặc từ hoặc bất cứ thứ gì tạo thành các hướng dẫn và nghĩa đen và bất kỳ dữ liệu nào. Chúng tôi chỉ bắt đầu phân bổ bộ nhớ và xây dựng các giá trị sẽ tạo ra chương trình khi chúng tôi đi và ghi lại bất kỳ nơi nào chúng tôi cần quay lại và sửa địa chỉ. Ở nơi đó, chúng tôi đặt một hình nộm để chỉ vị trí để chúng tôi có thể tiếp tục tính toán kích thước bộ nhớ. Ví dụ mã máy đầu tiên của chúng tôi có thể mất một ô. Mã máy tiếp theo có thể có 3 ô, bao gồm một ô mã máy và hai ô địa chỉ. Bây giờ con trỏ địa chỉ của chúng tôi là 4. Chúng tôi biết những gì đi trong ô máy, đó là mã op, nhưng chúng tôi phải chờ để tính toán những gì đi trong các ô địa chỉ cho đến khi chúng tôi biết dữ liệu đó sẽ được đặt ở đâu, tức là

Nếu chỉ có một tệp nguồn, về mặt lý thuyết, trình biên dịch có thể tạo mã máy hoàn toàn thực thi mà không cần trình liên kết. Trong quy trình hai lượt, nó có thể tính toán tất cả các địa chỉ thực tế cho tất cả các ô dữ liệu được tham chiếu bởi bất kỳ tải máy hoặc hướng dẫn lưu trữ nào. Và nó có thể tính toán tất cả các địa chỉ tuyệt đối được tham chiếu bởi bất kỳ hướng dẫn nhảy tuyệt đối nào. Đây là cách trình biên dịch đơn giản hơn, giống như trình biên dịch trong Forth, không có trình liên kết.

Một trình liên kết là một cái gì đó cho phép các khối mã được biên dịch riêng. Điều này có thể tăng tốc quá trình xây dựng mã tổng thể và cho phép linh hoạt với cách sử dụng các khối sau này, nói cách khác, chúng có thể được định vị lại trong bộ nhớ, ví dụ: thêm 1000 vào mỗi địa chỉ để tăng khối cho 1000 ô địa chỉ.

Vì vậy, những gì trình biên dịch xuất ra là mã máy thô chưa được xây dựng đầy đủ, nhưng được trình bày để chúng tôi biết kích thước của mọi thứ, nói cách khác để chúng tôi có thể bắt đầu tính toán vị trí của tất cả các địa chỉ tuyệt đối. trình biên dịch cũng đưa ra một danh sách các ký hiệu là các cặp tên / địa chỉ. Các ký hiệu liên quan đến phần bù bộ nhớ trong mã máy trong mô-đun có tên. Giá trị bù là khoảng cách tuyệt đối đến vị trí bộ nhớ của ký hiệu trong mô-đun.

Đó là nơi chúng tôi nhận được để liên kết. Trình liên kết trước tiên sẽ tát tất cả các khối mã máy này từ đầu đến cuối và ghi chú nơi mỗi khối bắt đầu. Sau đó, nó tính toán các địa chỉ sẽ được cố định bằng cách thêm phần bù tương đối trong một mô-đun và vị trí tuyệt đối của mô-đun trong bố cục lớn hơn.

Rõ ràng là tôi đã quá đơn giản hóa điều này để bạn có thể cố gắng nắm bắt nó và tôi đã cố tình không sử dụng thuật ngữ của các tệp đối tượng, bảng biểu tượng, v.v ... mà đối với tôi là một phần của sự nhầm lẫn.


13

GCC biên dịch chương trình C / C ++ thành 4 phần thực thi.

Ví dụ, gcc -o hello hello.cđược thực hiện như sau:

1. Tiền xử lý

Tiền xử lý thông qua Bộ xử lý GNU C ( cpp.exe), bao gồm các tiêu đề ( #include) và mở rộng các macro ( #define).

cpp hello.c > hello.i

Tệp trung gian kết quả "hello.i" chứa mã nguồn mở rộng.

2. Biên soạn

Trình biên dịch biên dịch mã nguồn được xử lý trước thành mã lắp ráp cho một bộ xử lý cụ thể.

gcc -S hello.i

Tùy chọn -S chỉ định để tạo mã lắp ráp, thay vì mã đối tượng. Tập tin lắp ráp kết quả là "hello.s".

3. Hội

Trình biên dịch ( as.exe) chuyển đổi mã lắp ráp thành mã máy trong tệp đối tượng "hello.o".

as -o hello.o hello.s

4. Trình liên kết

Cuối cùng, trình liên kết ( ld.exe) liên kết mã đối tượng với mã thư viện để tạo ra tệp thực thi "xin chào".

    ld -o xin chào hello.o ... thư viện ...

9

Nhìn vào URL: http://facemony.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Quy trình biên dịch hoàn chỉnh của C ++ được giới thiệu rõ ràng trong URL này.


2
Cảm ơn đã chia sẻ điều đó, thật đơn giản và dễ hiểu.
Đánh dấu

Tốt, tài nguyên, bạn có thể đặt một số giải thích cơ bản về quy trình ở đây không, câu trả lời được đánh dấu bằng thuật toán là chất lượng thấp b / c nó ngắn và chỉ là url.
JasonB

Một hướng dẫn ngắn gọn mà tôi đã tìm thấy: calleerlandsson.com/the-four-stages-of-compiling-ac-program
Guy Avraham
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.