Làm thế nào để viết một trình biên dịch rất cơ bản


214

Trình biên dịch nâng cao như gccbiên dịch mã thành các tệp có thể đọc được theo ngôn ngữ mà mã đã được viết (ví dụ: C, C ++, v.v.). Trong thực tế, họ giải thích ý nghĩa của từng mã theo thư viện và chức năng của các ngôn ngữ tương ứng. Sửa lỗi cho tôi nếu tôi sai.

Tôi muốn hiểu rõ hơn về trình biên dịch bằng cách viết một trình biên dịch rất cơ bản (có thể bằng C) để biên dịch một tệp tĩnh (ví dụ Hello World trong tệp văn bản). Tôi đã thử một số hướng dẫn và sách, nhưng tất cả chúng đều dành cho các trường hợp thực tế. Họ đối phó với việc biên dịch mã động với ý nghĩa được kết nối với ngôn ngữ tương ứng.

Làm cách nào tôi có thể viết trình biên dịch cơ bản để chuyển đổi văn bản tĩnh thành tệp có thể đọc được trên máy?

Bước tiếp theo sẽ giới thiệu các biến vào trình biên dịch; hãy tưởng tượng rằng chúng ta muốn viết một trình biên dịch chỉ biên dịch một số chức năng của ngôn ngữ.

Giới thiệu các hướng dẫn và tài nguyên thực tế được đánh giá cao :-)


6
Bạn có thấy lập trình
Mat

Bạn đã thử lex / flex và yacc / bison chưa?
mouviciel

15
@mouviciel: Đó không phải là cách hay để tìm hiểu về việc xây dựng trình biên dịch. Những công cụ đó làm rất nhiều công việc khó khăn cho bạn, vì vậy bạn không bao giờ thực sự làm điều đó và tìm hiểu cách nó được thực hiện.
Mason Wheeler

11
@Mat thú vị, đầu tiên các liên kết của bạn cung cấp 404, trong khi liên kết thứ hai hiện được đánh dấu là trùng lặp với câu hỏi này .
Ruslan

Câu trả lời:


326

Giới thiệu

Một trình biên dịch điển hình thực hiện các bước sau:

  • Phân tích cú pháp: văn bản nguồn được chuyển đổi thành cây cú pháp trừu tượng (AST).
  • Độ phân giải của các tham chiếu đến các mô-đun khác (C hoãn lại bước này cho đến khi liên kết).
  • Xác nhận ngữ nghĩa: loại bỏ các tuyên bố chính xác về mặt cú pháp mà không có ý nghĩa, ví dụ mã không thể truy cập hoặc khai báo trùng lặp.
  • Các phép biến đổi tương đương và tối ưu hóa mức cao: AST được biến đổi để thể hiện một tính toán hiệu quả hơn với cùng một ngữ nghĩa. Điều này bao gồm, ví dụ tính toán sớm các biểu thức con phổ biến và biểu thức hằng, loại bỏ các bài tập cục bộ quá mức (xem thêm SSA ), v.v.
  • Tạo mã: AST được chuyển đổi thành mã cấp thấp tuyến tính, với các bước nhảy, phân bổ đăng ký và tương tự. Một số cuộc gọi chức năng có thể được nội tuyến ở giai đoạn này, một số vòng lặp không được kiểm soát, v.v.
  • Tối ưu hóa lổ nhìn trộm: mã cấp thấp được quét cho các hiệu quả cục bộ đơn giản được loại bỏ.

Hầu hết các trình biên dịch hiện đại (ví dụ, gcc và clang) lặp lại hai bước cuối cùng một lần nữa. Họ sử dụng một ngôn ngữ trung gian thấp nhưng độc lập với nền tảng để tạo mã ban đầu. Sau đó, ngôn ngữ đó được chuyển đổi thành mã dành riêng cho nền tảng (x86, ARM, v.v.) thực hiện điều tương tự theo cách tối ưu hóa nền tảng. Điều này bao gồm, ví dụ như việc sử dụng các hướng dẫn vectơ khi có thể, sắp xếp lại hướng dẫn để tăng hiệu quả dự đoán nhánh, v.v.

Sau đó, mã đối tượng đã sẵn sàng để liên kết. Hầu hết các trình biên dịch mã gốc đều biết cách gọi một trình liên kết để tạo ra một tệp thực thi, nhưng đó không phải là một bước biên dịch mỗi se. Trong các ngôn ngữ như liên kết Java và C # có thể hoàn toàn động, được thực hiện bởi VM khi tải.

Ghi nhớ những điều cơ bản

  • Lam cho no hoạt động
  • Làm cho nó đẹp
  • Làm cho nó hiệu quả

Trình tự cổ điển này áp dụng cho tất cả các phát triển phần mềm, nhưng có sự lặp lại.

Tập trung vào bước đầu tiên của chuỗi. Tạo điều đơn giản nhất có thể có thể làm việc.

Đọc những cuốn sách!

Đọc Sách Rồng của Aho và Ullman. Điều này là cổ điển và vẫn còn khá áp dụng ngày nay.

Thiết kế trình biên dịch hiện đại cũng được ca ngợi.

Nếu công cụ này quá khó đối với bạn ngay bây giờ, hãy đọc một số phần giới thiệu về phân tích cú pháp trước; thường phân tích các thư viện bao gồm phần giới thiệu và ví dụ.

Hãy chắc chắn rằng bạn cảm thấy thoải mái khi làm việc với đồ thị, đặc biệt là cây cối. Những điều này là các chương trình công cụ được thực hiện ở mức độ logic.

Xác định ngôn ngữ của bạn tốt

Sử dụng bất kỳ ký hiệu nào bạn muốn, nhưng hãy chắc chắn rằng bạn có một mô tả đầy đủ và nhất quán về ngôn ngữ của bạn. Điều này bao gồm cả cú pháp và ngữ nghĩa.

Đã đến lúc viết đoạn mã bằng ngôn ngữ mới của bạn làm trường hợp thử nghiệm cho trình biên dịch trong tương lai.

Sử dụng ngôn ngữ yêu thích của bạn

Hoàn toàn ổn khi viết trình biên dịch bằng Python hoặc Ruby hoặc bất kỳ ngôn ngữ nào đều dễ dàng đối với bạn. Sử dụng các thuật toán đơn giản, bạn hiểu rõ. Phiên bản đầu tiên không nhất thiết phải nhanh, hiệu quả hoặc đầy đủ tính năng. Nó chỉ cần phải đủ chính xác và dễ dàng để sửa đổi.

Bạn cũng có thể viết các giai đoạn khác nhau của trình biên dịch bằng các ngôn ngữ khác nhau, nếu cần.

Chuẩn bị viết rất nhiều bài kiểm tra

Toàn bộ ngôn ngữ của bạn nên được bao phủ bởi các trường hợp thử nghiệm; có hiệu quả nó sẽ được xác định bởi họ. Làm quen với khung thử nghiệm ưa thích của bạn. Viết bài kiểm tra từ ngày đầu tiên. Tập trung vào các thử nghiệm 'dương' chấp nhận mã chính xác, trái ngược với việc phát hiện mã không chính xác.

Chạy tất cả các bài kiểm tra thường xuyên. Sửa các bài kiểm tra bị hỏng trước khi tiến hành. Sẽ là một sự xấu hổ khi kết thúc với một ngôn ngữ không xác định không thể chấp nhận mã hợp lệ.

Tạo một trình phân tích cú pháp tốt

Máy phát điện Parser rất nhiều . Chọn bất cứ thứ gì bạn muốn. Bạn cũng có thể viết phân tích cú pháp của riêng bạn từ đầu, nhưng nó chỉ có giá trị nó nếu cú pháp của ngôn ngữ của bạn là chết đơn giản.

Trình phân tích cú pháp sẽ phát hiện và báo cáo lỗi cú pháp. Viết rất nhiều trường hợp kiểm tra, cả tích cực và tiêu cực; sử dụng lại mã bạn đã viết trong khi xác định ngôn ngữ.

Đầu ra của trình phân tích cú pháp của bạn là một cây cú pháp trừu tượng.

Nếu ngôn ngữ của bạn có các mô-đun, đầu ra của trình phân tích cú pháp có thể là biểu diễn đơn giản nhất của 'mã đối tượng' mà bạn tạo. Có rất nhiều cách đơn giản để kết xuất cây vào một tệp và nhanh chóng tải lại.

Tạo một trình xác nhận ngữ nghĩa

Rất có thể ngôn ngữ của bạn cho phép các cấu trúc chính xác về mặt cú pháp có thể không có ý nghĩa trong các bối cảnh nhất định. Một ví dụ là một khai báo trùng lặp của cùng một biến hoặc truyền tham số của một loại sai. Trình xác nhận sẽ phát hiện các lỗi như vậy nhìn vào cây.

Trình xác nhận cũng sẽ giải quyết các tham chiếu đến các mô-đun khác được viết bằng ngôn ngữ của bạn, tải các mô-đun khác này và sử dụng trong quy trình xác thực. Ví dụ, bước này sẽ đảm bảo rằng số lượng tham số được truyền cho một chức năng từ một mô-đun khác là chính xác.

Một lần nữa, viết và chạy rất nhiều trường hợp thử nghiệm. Các trường hợp tầm thường là không thể thiếu trong việc khắc phục sự cố là thông minh và phức tạp.

Tạo mã

Sử dụng các kỹ thuật đơn giản nhất mà bạn biết. Thông thường, có thể dịch trực tiếp một cấu trúc ngôn ngữ (như một ifcâu lệnh) sang một mẫu mã được tham số hóa nhẹ, không giống như một mẫu HTML.

Một lần nữa, bỏ qua hiệu quả và tập trung vào tính chính xác.

Nhắm mục tiêu VM cấp thấp độc lập với nền tảng

Tôi cho rằng bạn bỏ qua những thứ cấp thấp trừ khi bạn quan tâm sâu sắc đến các chi tiết cụ thể về phần cứng. Những chi tiết này là đẫm máu và phức tạp.

Lựa chọn của bạn:

  • LLVM: cho phép tạo mã máy hiệu quả, thường là cho x86 và ARM.
  • CLR: nhắm mục tiêu .NET, chủ yếu dựa trên x86 / Windows; có JIT tốt.
  • JVM: nhắm mục tiêu thế giới Java, khá đa nền tảng, có JIT tốt.

Bỏ qua tối ưu hóa

Tối ưu hóa là khó. Hầu như luôn luôn tối ưu hóa là sớm. Tạo mã không hiệu quả nhưng đúng. Thực hiện toàn bộ ngôn ngữ trước khi bạn cố gắng tối ưu hóa mã kết quả.

Tất nhiên, tối ưu hóa tầm thường là OK để giới thiệu. Nhưng tránh mọi thứ xảo quyệt, nhiều lông trước khi trình biên dịch của bạn ổn định.

Vậy thì sao?

Nếu tất cả những thứ này không quá đáng sợ đối với bạn, vui lòng tiếp tục! Đối với một ngôn ngữ đơn giản, mỗi bước có thể đơn giản hơn bạn nghĩ.

Xem 'Thế giới xin chào' từ một chương trình mà trình biên dịch của bạn tạo ra có thể đáng để nỗ lực.


45
Đây là một trong những câu trả lời hay nhất tôi từng thấy.
gahooa

11
Tôi nghĩ rằng bạn đã bỏ lỡ một phần của câu hỏi ... OP muốn viết một trình biên dịch rất cơ bản . Tôi nghĩ rằng bạn đi xa hơn rất cơ bản ở đây.
marco-fiset

22
@ marco-fiset , ngược lại, tôi nghĩ đó là một câu trả lời nổi bật cho OP biết cách thực hiện một trình biên dịch rất cơ bản, trong khi chỉ ra các bẫy để tránh và xác định các giai đoạn nâng cao hơn.
smci

6
Đây là một trong những câu trả lời hay nhất tôi từng thấy trong toàn bộ vũ trụ Stack Exchange. Thanh danh!
Andre Terra

3
Xem 'Thế giới xin chào' từ một chương trình mà trình biên dịch của bạn tạo ra có thể đáng để nỗ lực. - INDEED
slier

27

Jack Crenshaw's Let's Build a Compiler , trong khi chưa hoàn thành, là phần giới thiệu và hướng dẫn dễ đọc.

Nicklaus Wirth's Compiler Construction là một cuốn sách giáo khoa rất hay về những điều cơ bản của việc xây dựng trình biên dịch đơn giản. Anh ta tập trung vào dòng dõi đệ quy từ trên xuống, mà, hãy đối mặt với nó, RẤT dễ dàng hơn lex / yacc hoặc flex / bison. Trình biên dịch PASCAL ban đầu mà nhóm của ông đã viết được thực hiện theo cách này.

Những người khác đã đề cập đến các cuốn sách Rồng khác nhau.


1
Một trong những điều hay về Pascal là mọi thứ phải được xác định hoặc khai báo trước khi sử dụng. Do đó, nó có thể được biên dịch trong một lần. Turbo Pascal 3.0 là một ví dụ như vậy, và có rất nhiều tài liệu về nội bộ ở đây .
tcrosley

1
PASCAL được thiết kế đặc biệt với tính năng biên dịch và liên kết một lượt. Cuốn sách trình biên dịch của Wirth có đề cập đến nhiều trình biên dịch, và thêm rằng anh ta biết về trình biên dịch PL / I đã mất 70 (vâng, bảy mươi) lượt.
John R. Strohm

Khai báo bắt buộc trước khi sử dụng ngày trở lại ALGOL. Tony Hoare bị tai của ủy ban ALGOL ghim lại khi anh ta cố gắng đề xuất thêm các quy tắc loại mặc định, tương tự như những gì FORTRAN có. Họ đã biết về các vấn đề mà điều này có thể tạo ra, với các lỗi đánh máy trong tên và quy tắc mặc định tạo ra các lỗi thú vị.
John R. Strohm

1
Dưới đây là phiên bản cập nhật và hoàn thiện của cuốn sách của chính tác giả: stack.nl/~marcov/compiler.pdf Vui lòng chỉnh sửa câu trả lời của bạn và thêm phần này :)
sonnet

16

Tôi thực sự bắt đầu với việc viết một trình biên dịch cho Brainfuck . Đây là một ngôn ngữ khá khó hiểu để lập trình nhưng nó chỉ có 8 hướng dẫn để thực hiện. Nó đơn giản như bạn có thể nhận được và có các lệnh C tương đương ngoài kia cho các lệnh liên quan nếu bạn tìm thấy cú pháp tắt.


7
Nhưng sau đó, khi bạn đã có trình biên dịch BF sẵn sàng, bạn phải viết mã của mình vào đó :(
500 - Lỗi máy chủ nội bộ

@ 500-InternalServerError sử dụng phương pháp tập hợp con C
Kỹ sư thế giới

12

Nếu bạn thực sự chỉ muốn viết mã máy có thể đọc được và không nhắm mục tiêu vào máy ảo, thì bạn sẽ phải đọc hướng dẫn sử dụng Intel và hiểu

  • a. Liên kết và tải mã thực thi

  • b. Các định dạng COFF và PE (cho các cửa sổ), thay vào đó là định dạng ELF (cho Linux)

  • c. Hiểu các định dạng tệp .COM (dễ dàng hơn PE)
  • Cười mở miệng. Hiểu lắp ráp
  • e. Hiểu trình biên dịch và công cụ tạo mã trong trình biên dịch.

Khó hơn nhiều so với nói. Tôi khuyên bạn nên đọc Trình biên dịch và Phiên dịch trong C ++ làm điểm bắt đầu (Tác giả Ronald Mak). Ngoài ra, "cho phép xây dựng trình biên dịch" của Crenshaw là OK.

Nếu bạn không muốn làm điều đó, bạn cũng có thể viết VM của riêng mình và viết một trình tạo mã được nhắm mục tiêu đến VM đó.

Lời khuyên: Tìm hiểu Flex và Bison FIRST. Sau đó tiếp tục xây dựng trình biên dịch / VM của riêng bạn.

Chúc may mắn!


7
Tôi nghĩ rằng nhắm mục tiêu LLVM và không phải mã máy thực sự là cách tốt nhất hiện nay.
9000

Tôi đồng ý, đôi khi tôi đã theo dõi LLVM và tôi nên nói rằng đó là một trong những điều tốt nhất tôi đã thấy trong nhiều năm về nỗ lực lập trình viên cần thiết để nhắm mục tiêu!
Aniket Inge

2
Điều gì về MIPS và sử dụng spim để chạy nó? Hay MIX ?

@MichaelT Tôi chưa sử dụng MIPS nhưng tôi chắc chắn nó sẽ tốt.
Aniket Inge

Tập lệnh @PrototypeStark RISC, bộ xử lý trong thế giới thực vẫn còn được sử dụng cho đến ngày nay (hiểu rằng nó sẽ được dịch sang các hệ thống nhúng). Bộ hướng dẫn đầy đủ có tại wikipedia . Nhìn trên mạng, có rất nhiều ví dụ và nó được sử dụng trong nhiều lớp học thuật làm mục tiêu cho lập trình ngôn ngữ máy. Có một chút hoạt động về nó tại SO .

10

Cách tiếp cận DIY cho trình biên dịch đơn giản có thể trông như thế này (ít nhất đó là cách dự án uni của tôi trông như thế nào):

  1. Xác định ngữ pháp của ngôn ngữ. Bối cảnh miễn phí.
  2. Nếu ngữ pháp của bạn chưa phải là LL (1), hãy thực hiện ngay bây giờ. Lưu ý rằng một số quy tắc có vẻ ổn trong ngữ pháp CF đơn giản có thể trở nên xấu xí. Có lẽ ngôn ngữ của bạn quá phức tạp ...
  3. Viết Lexer cắt luồng văn bản thành mã thông báo (từ, số, chữ).
  4. Viết trình phân tích cú pháp gốc đệ quy từ trên xuống cho ngữ pháp của bạn, chấp nhận hoặc từ chối nhập liệu.
  5. Thêm thế hệ cây cú pháp vào trình phân tích cú pháp của bạn.
  6. Viết trình tạo mã máy từ cây cú pháp.
  7. Lợi nhuận & Bia, thay vào đó, bạn có thể bắt đầu suy nghĩ làm thế nào để phân tích cú pháp thông minh hơn hoặc tạo mã tốt hơn.

Cần có nhiều tài liệu mô tả từng bước một cách chi tiết.


Điểm thứ 7 là những gì OP đang hỏi về.
Florian Margaine

7
1-5 là không liên quan và không xứng đáng được chú ý như vậy. 6 là phần thú vị nhất. Thật không may, hầu hết các cuốn sách đều theo cùng một mô hình, sau cuốn sách rồng khét tiếng, quá chú ý đến việc phân tích cú pháp và để lại các biến đổi mã ngoài phạm vi.
SK-logic
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.