วิธีการเขียนคอมไพเลอร์ขั้นพื้นฐานมาก


214

คอมไพเลอร์ขั้นสูงเช่นgccรหัสคอมไพล์ลงในไฟล์ที่เครื่องสามารถอ่านได้ตามภาษาที่เขียนโค้ดนั้น (เช่น C, C ++ เป็นต้น) ในความเป็นจริงพวกเขาตีความความหมายของแต่ละรหัสตามห้องสมุดและฟังก์ชั่นของภาษาที่เกี่ยวข้อง ช่วยแก้ให้ด้วยนะถ้าฉันผิด.

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

ฉันจะเขียนคอมไพเลอร์พื้นฐานเพื่อแปลงข้อความคงเป็นไฟล์ที่เครื่องอ่านได้อย่างไร

ขั้นตอนต่อไปจะแนะนำตัวแปรในคอมไพเลอร์ ลองนึกภาพว่าเราต้องการเขียนคอมไพเลอร์ที่รวบรวมเฉพาะบางฟังก์ชั่นของภาษา

ขอแนะนำบทเรียนและแหล่งข้อมูลเชิงปฏิบัติที่ได้รับความนิยมอย่างสูง :-)



คุณลอง lex / flex และ yacc / bison แล้วหรือยัง?
mouviciel

15
@mouviciel: นั่นไม่ใช่วิธีที่ดีในการเรียนรู้เกี่ยวกับการสร้างคอมไพเลอร์ เครื่องมือเหล่านั้นทำงานหนักสำหรับคุณดังนั้นคุณจึงไม่เคยทำมันและเรียนรู้วิธีการทำงานของมัน
Mason Wheeler

11
@Mat ที่น่าสนใจลิงค์แรกของคุณมี 404 ขณะที่ลิงก์ที่สองถูกทำเครื่องหมายว่าซ้ำกับคำถามนี้
Ruslan

คำตอบ:


326

Intro

คอมไพเลอร์ทั่วไปทำตามขั้นตอนต่อไปนี้:

  • การแยกวิเคราะห์: ข้อความต้นฉบับจะถูกแปลงเป็นโครงสร้างไวยากรณ์นามธรรม (AST)
  • ความละเอียดของการอ้างอิงไปยังโมดูลอื่น (C เลื่อนขั้นตอนนี้จนถึงการเชื่อมโยง)
  • การตรวจสอบความหมาย: กำจัดข้อความที่ถูกต้องทางไวยากรณ์ซึ่งไม่สมเหตุสมผลเช่นรหัสที่ไม่สามารถเข้าถึงได้หรือการประกาศที่ซ้ำกัน
  • การแปลงที่เท่าเทียมกันและการปรับให้เหมาะสมในระดับสูง: AST ถูกแปลงเพื่อแสดงการคำนวณที่มีประสิทธิภาพมากขึ้นด้วยความหมายเดียวกัน ซึ่งรวมถึงเช่นการคำนวณต้นของนิพจน์ย่อยทั่วไปและนิพจน์คงที่การกำจัดการมอบหมายในท้องถิ่นมากเกินไป (ดูSSA ) เป็นต้น
  • การสร้างรหัส: AST ถูกแปลงเป็นรหัสระดับต่ำเชิงเส้นพร้อมการกระโดดการจัดสรรการลงทะเบียนและสิ่งที่คล้ายกัน การเรียกฟังก์ชั่นบางอย่างสามารถอินไลน์ได้ในขั้นตอนนี้บางลูปที่ไม่ได้ควบคุม ฯลฯ
  • การเพิ่มประสิทธิภาพตาแมว: สแกนรหัสระดับต่ำเพื่อความไร้ประสิทธิภาพในท้องถิ่นที่ง่ายซึ่งจะถูกกำจัด

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

หลังจากนั้นรหัสวัตถุก็พร้อมสำหรับการเชื่อมโยงแล้ว คอมไพเลอร์โค้ดเนทีฟส่วนใหญ่รู้วิธีเรียก linker เพื่อสร้างไฟล์ที่เรียกใช้งานได้ แต่ไม่ใช่ขั้นตอนการคอมไพล์ต่อ ในภาษาเช่นการเชื่อมโยง Java และ C # อาจเป็นแบบไดนามิกทั้งหมดทำโดย VM ที่เวลาโหลด

จดจำพื้นฐาน

  • ทำให้มันใช้งานได้
  • ทำให้สวย
  • ทำให้มีประสิทธิภาพ

ลำดับคลาสสิกนี้ใช้กับการพัฒนาซอฟต์แวร์ทั้งหมด แต่มีการกล่าวซ้ำ ๆ

มีสมาธิในขั้นตอนแรกของลำดับ สร้างสิ่งที่ง่ายที่สุดที่อาจเป็นไปได้

อ่านหนังสือ!

อ่านDragon Bookโดย Aho และ Ullman นี่เป็นแบบคลาสสิคและยังคงใช้งานได้ดีในปัจจุบัน

การออกแบบคอมไพเลอร์สมัยใหม่ยังได้รับการยกย่อง

หากสิ่งนี้ยากเกินไปสำหรับคุณในตอนนี้ให้อ่าน intros แยกวิเคราะห์ก่อน โดยทั่วไปการแยกวิเคราะห์ไลบรารี่จะรวมถึง intros และตัวอย่าง

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

กำหนดภาษาของคุณได้ดี

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

ถึงเวลาแล้วที่จะเขียนตัวอย่างโค้ดในภาษาใหม่ของคุณเป็นกรณีทดสอบสำหรับคอมไพเลอร์ในอนาคต

ใช้ภาษาที่คุณชื่นชอบ

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

การเขียนคอมไพเลอร์เป็นภาษาต่าง ๆ ถ้าจำเป็น

เตรียมเขียนข้อสอบมากมาย

ภาษาทั้งหมดของคุณควรได้รับการคุ้มครองโดยกรณีทดสอบ อย่างมีประสิทธิภาพมันจะถูกกำหนดโดยพวกเขา ทำความคุ้นเคยกับกรอบการทดสอบที่คุณต้องการ เขียนข้อสอบตั้งแต่วันแรก เน้นการทดสอบ 'บวก' ที่ยอมรับรหัสที่ถูกต้องซึ่งต่างจากการตรวจจับรหัสที่ไม่ถูกต้อง

ทำการทดสอบทั้งหมดเป็นประจำ แก้ไขการทดสอบที่เสียหายก่อนดำเนินการต่อ มันน่าละอายที่จะจบลงด้วยภาษาที่ไม่เหมาะสมซึ่งไม่สามารถรับรหัสที่ถูกต้องได้

สร้างโปรแกรมแยกวิเคราะห์ที่ดี

เครื่องกำเนิดไฟฟ้าตัวแยกวิเคราะห์เป็นจำนวนมาก เลือกสิ่งที่คุณต้องการ คุณอาจเขียน parser ของคุณเองตั้งแต่เริ่มต้น แต่มันก็คุ้มค่าถ้าไวยากรณ์ของภาษาของคุณตายง่าย

โปรแกรมวิเคราะห์คำควรตรวจจับและรายงานข้อผิดพลาดทางไวยากรณ์ เขียนกรณีทดสอบจำนวนมากทั้งบวกและลบ ใช้รหัสที่คุณเขียนซ้ำในขณะที่กำหนดภาษา

ผลลัพธ์ของ parser ของคุณเป็นต้นไม้ที่เป็นนามธรรม

หากภาษาของคุณมีโมดูลผลลัพธ์ของ parser อาจเป็นการแสดงที่ง่ายที่สุดของ 'รหัสวัตถุ' ที่คุณสร้าง มีวิธีง่าย ๆ มากมายในการถ่ายโอนต้นไม้ไปยังไฟล์และโหลดกลับอย่างรวดเร็ว

สร้างตัวตรวจสอบความหมาย

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

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

อีกครั้งเขียนและเรียกใช้กรณีทดสอบจำนวนมาก กรณีเล็ก ๆ น้อย ๆ ที่ขาดไม่ได้ในการแก้ไขปัญหาที่ฉลาดและซับซ้อน

สร้างรหัส

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

อีกครั้งให้ละเว้นประสิทธิภาพและมุ่งเน้นไปที่ความถูกต้อง

กำหนดเป้าหมาย VM ระดับต่ำที่ไม่ขึ้นกับแพลตฟอร์ม

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

ทางเลือกของคุณ:

  • LLVM: ช่วยให้สามารถสร้างรหัสเครื่องได้อย่างมีประสิทธิภาพโดยปกติคือ x86 และ ARM
  • CLR: เป้าหมาย. NET, ส่วนใหญ่ใช้ x86 / Windows; มี JIT ที่ดี
  • JVM: ตั้งเป้าไปที่โลก Java ซึ่งค่อนข้างหลากหลายแพลตฟอร์มมี JIT ที่ดี

ละเว้นการเพิ่มประสิทธิภาพ

การเพิ่มประสิทธิภาพเป็นเรื่องยาก การเพิ่มประสิทธิภาพเกือบทุกครั้งเป็นสิ่งที่เกิดก่อนกำหนด สร้างรหัสที่ไม่มีประสิทธิภาพ แต่ถูกต้อง ใช้ภาษาทั้งหมดก่อนที่จะลองปรับรหัสผลลัพธ์ให้เหมาะสม

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

แล้วอะไรล่ะ

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

การเห็น 'Hello world' จากโปรแกรมที่คอมไพเลอร์ของคุณสร้างขึ้นอาจคุ้มค่ากับความพยายาม


45
นี่เป็นหนึ่งในคำตอบที่ดีที่สุดที่ฉันเคยเห็น
gahooa

11
ฉันคิดว่าคุณพลาดคำถามไปส่วนหนึ่ง ... ผู้ปฏิบัติการต้องการเขียนคอมไพเลอร์พื้นฐานมากๆ ฉันคิดว่าคุณไปไกลกว่าขั้นพื้นฐานมากที่นี่
marco-fiset

22
@ marco-fisetตรงกันข้ามฉันคิดว่ามันเป็นคำตอบที่โดดเด่นที่บอก OP ถึงวิธีการทำคอมไพเลอร์ขั้นพื้นฐานมากในขณะที่ชี้ไปที่กับดักเพื่อหลีกเลี่ยงและกำหนดขั้นตอนที่สูงขึ้น
smci

6
นี่เป็นหนึ่งในคำตอบที่ดีที่สุดที่ฉันเคยเห็นในจักรวาลสแต็ก Exchange ทั้งหมด รุ่งโรจน์!
Andre Terra

3
การเห็น 'Hello world' จากโปรแกรมที่คอมไพเลอร์ของคุณสร้างขึ้นอาจคุ้มค่ากับความพยายาม -
INDEED

27

Let's สร้างคอมไพเลอร์ของ Jack Crenshaw ในขณะที่ยังไม่เสร็จเป็นบทแนะนำและบทแนะนำที่อ่านได้อย่างชัดเจน

โครงสร้างคอมไพเลอร์ของ Nicklaus Wirth เป็นหนังสือเรียนที่ดีมากเกี่ยวกับพื้นฐานของการสร้างคอมไพเลอร์อย่างง่าย เขามุ่งเน้นไปที่การสืบเชื้อสายจากบนลงล่างซึ่งเรามาเผชิญหน้ากันมันง่ายกว่า lex / yacc หรือ flex / bison คอมไพเลอร์ PASCAL ดั้งเดิมที่กลุ่มของเขาเขียนเสร็จแล้วด้วยวิธีนี้

คนอื่น ๆ ได้พูดถึงหนังสือมังกรต่าง ๆ


1
หนึ่งในสิ่งที่ดีเกี่ยวกับ Pascal คือทุกอย่างจะต้องมีการกำหนดหรือประกาศก่อนที่จะถูกนำมาใช้ ดังนั้นจึงสามารถรวบรวมได้ในครั้งเดียว เทอร์โบปาสกาล 3.0 เป็นตัวอย่างหนึ่งเช่นและมีเป็นจำนวนมากของเอกสารเกี่ยวกับ internals ที่นี่
tcrosley

1
PASCAL ได้รับการออกแบบมาโดยเฉพาะด้วยการรวบรวมและส่งผ่านข้อมูลแบบ one-pass หนังสือผู้เรียบเรียงของ Wirth กล่าวถึงคอมไพเลอร์หลายตัวและเสริมว่าเขารู้คอมไพเลอร์ PL / I ที่ใช้เวลา 70 (ใช่เจ็ดสิบ)
John R. Strohm

การประกาศบังคับก่อนวันใช้งานกลับไปที่ ALGOL Tony Hoare ได้หูของเขากลับมาโดยคณะกรรมการ ALGOL เมื่อเขาพยายามแนะนำการเพิ่มกฎประเภทเริ่มต้นคล้ายกับสิ่งที่ FORTRAN มี พวกเขารู้แล้วเกี่ยวกับปัญหาที่อาจเกิดขึ้นกับข้อผิดพลาดในการพิมพ์ในชื่อและกฎเริ่มต้นการสร้างข้อบกพร่องที่น่าสนใจ
John R. Strohm

1
นี่คือรุ่นหนังสือที่ปรับปรุงและเสร็จสิ้นมากขึ้นโดยผู้แต่งต้นฉบับเอง: stack.nl/~marcov/compiler.pdf โปรดแก้ไขคำตอบของคุณและเพิ่มสิ่งนี้ :)
sonnet

16

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


7
แต่เมื่อคุณมีคอมไพเลอร์ BF ของคุณแล้วคุณต้องเขียนโค้ดของคุณ :(
500 - ข้อผิดพลาดเซิร์ฟเวอร์ภายใน

@ 500-InternalServerError ใช้วิธีการย่อย C
World Engineer

12

หากคุณต้องการเขียนโค้ดที่อ่านได้ของเครื่องเท่านั้นและไม่ได้กำหนดเป้าหมายไปที่เครื่องเสมือนคุณจะต้องอ่านคู่มือ Intel และเข้าใจ

  • การเชื่อมโยงและการโหลดรหัสที่ปฏิบัติการได้

  • ข รูปแบบ COFF และ PE (สำหรับ windows) หรือทำความเข้าใจกับรูปแบบ ELF (สำหรับ Linux)

  • ค ทำความเข้าใจกับรูปแบบไฟล์. COM (ง่ายกว่า PE)
  • d ทำความเข้าใจกับผู้ประกอบ
  • อี ทำความเข้าใจกับคอมไพเลอร์และเอ็นจิ้นการสร้างรหัสในคอมไพเลอร์

ทำได้ยากกว่าที่คิดไว้มาก ฉันแนะนำให้คุณอ่าน Compilers และ Interpreters ใน C ++ เป็นจุดเริ่มต้น (โดย Ronald Mak) อีกวิธีหนึ่ง "ให้สร้างคอมไพเลอร์" โดย Crenshaw ก็โอเค

หากคุณไม่ต้องการทำเช่นนั้นคุณสามารถเขียน VM ของคุณเองและเขียนโปรแกรมสร้างโค้ดที่กำหนดเป้าหมายไปที่ VM นั้นได้

เคล็ดลับ: เรียนรู้ Flex และ Bison FIRST จากนั้นไปสร้างคอมไพเลอร์ / VM ของคุณเอง

โชคดี!


7
ฉันคิดว่าการกำหนดเป้าหมาย LLVM ไม่ใช่รหัสเครื่องจริง ๆ เป็นวิธีที่ดีที่สุดในปัจจุบัน
9000

ฉันเห็นด้วยฉันได้ติดตาม LLVM มาระยะหนึ่งแล้วและฉันควรจะบอกว่ามันเป็นหนึ่งในสิ่งที่ดีที่สุดที่ฉันเคยเห็นในปีที่ผ่านมาในแง่ของความพยายามของโปรแกรมเมอร์ที่จำเป็นในการกำหนดเป้าหมาย!
Aniket Inge

2
MIPS เกี่ยวกับอะไรและใช้spimเพื่อเรียกใช้ หรือผสม ?

@MichaelT ฉันไม่ได้ใช้ MIPS แต่ฉันแน่ใจว่ามันจะดี
Aniket Inge

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

10

วิธี DIY สำหรับคอมไพเลอร์เรียบง่ายอาจมีลักษณะเช่นนี้ (อย่างน้อยนั่นก็คือโครงการ uni ของฉันที่ดูเหมือน)

  1. กำหนดไวยากรณ์ของภาษา บริบทฟรี
  2. หากไวยากรณ์ของคุณยังไม่ LL (1) ให้ทำทันที โปรดทราบว่ากฎบางอย่างที่ดูดีในไวยากรณ์ CF แบบธรรมดาอาจน่าเกลียด บางทีภาษาของคุณซับซ้อนเกินไป ...
  3. เขียน Lexer ซึ่งตัดกระแสข้อความเป็นโทเค็น (คำตัวเลขตัวอักษร)
  4. เขียนตัวแยกวิเคราะห์สืบเชื้อสายจากบนลงล่างสำหรับไวยากรณ์ของคุณซึ่งยอมรับหรือปฏิเสธอินพุต
  5. เพิ่มการสร้างแผนผังไวยากรณ์ลงใน parser ของคุณ
  6. เขียนตัวสร้างรหัสเครื่องจากแผนผังไวยากรณ์
  7. Profit & Beer หรือคุณสามารถเริ่มคิดวิธีการแยกวิเคราะห์อย่างชาญฉลาดหรือสร้างรหัสที่ดีกว่า

ควรมีวรรณกรรมมากมายที่อธิบายรายละเอียดแต่ละขั้นตอน


ประเด็นที่ 7 คือสิ่งที่ OP ถาม
Florian Margaine

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