Technology

เส้นทางสู่ Executable จาก Source Code เบื้องหลังของ Compiler

By Arnon Puitrakul - 30 มิถุนายน 2021 - 3 min read min(s)

เส้นทางสู่ Executable จาก Source Code เบื้องหลังของ Compiler

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

เริ่มต้นที่ Source Code จบที่ Executable

Application Compilation Process

ให้เรานึงถึงเวลาเราเขียน Code ผ่าน Text Editor หรือ IDE (Integrated Development Environment) เราก็เขียน ๆ ไป อาจจะให้มันคำนวณ หรือ ให้มันหาคำตอบอะไรบางอย่างให้เรา และเมื่อเรากด Run ใน IDE โปรแกรมมันก็โผล่ขึ้นมาทำงานให้เราเลย ทำให้มือใหม่หลาย ๆ คนเข้าใจว่า อ่อ เราเอาโปรแกรมที่เราเขียนนี่แหละ ไปรันทำงานได้เลย

แต่จริง ๆ แล้วมันไม่ได้เป็นแบบนั้นซะทีเดียว เพราะคอมพิวเตอร์อ่าน Code หรือที่เราเรียกว่า Source Code ไม่ออก ดังนั้น เราต้องการอะไรบางอย่างที่ช่วยแปลภาษา ระหว่าง Programming Language ที่เราเขียนกับภาษาที่เครื่องเข้าใจได้

ซึ่งสิ่งนั้นก็คือ Compiler นั่นเอง โดยที่ มันเป็น โปรแกรม ประเภทหนึ่ง ที่ทำหน้าที่ในการแปลง Source Code ให้กลายเป็น Machine Code เพื่อให้เครื่องอ่านได้ ทำให้ในละครเรื่องนี้ มีอยู่ทั้งหมด 3 ตัวละครด้วยกันคือ Source Code หรือก็คือ Code ที่เราเขียน, Compiler คนแปลงสาร และ Machine Code หรือก็คือ Code ที่เครื่องอ่านได้

Compiler ทำงานยังไง ?

Compiler Process
อันนี้เป็นแค่คร่าว ๆ เท่านั้น จริง ๆ มันยังมี Process อื่น ๆ เข้ามาอีกเช่น Code Optimisation

อย่างที่เราบอกไปว่า Compiler เป็นโปรแกรมที่แปลง Source Code ที่เราเขียนออกมาเป็น Machine Code เพื่อให้เครื่องอ่านได้ ดังนั้น ขั้นตอนแรกของ Compiler คือการอ่านไฟล์ Source Code ของเราออกมาก่อน

Lexical Analysis

ในการที่มันจะแปลภาษาออกมาได้ มันจะต้องแบ่งคำสั่งของเราออกมาเป็นส่วน ๆ ก่อน เหมือนที่เวลาเราเขียนโปรแกรม มันจะต้องมีคำสั่ง ด้านในก็จะมีพวก Argument ต่าง ๆ ที่เราเขียนลงไป ขั้นตอนนี้เราเรียกว่า Lexical Analysis มันจะทำการตัดพวกการเว้นวรรค พวกขึ้นบรรทัดใหม่ และ Comment ออก และพยายามที่จะ Tokenise หรือแบ่งคำสั่งออกมาเป็นชิ้น ๆ หลังจากได้ก่อนคำสั่งออกมาแล้วมันจะต้องเอามาคิดต่อแล้วว่าเราจะทำยังไงกับมันต่อดี

Syntactic Analysis
อันนี้ไม่ใช่ของจริงนะ จำลองขึ้นมาเพื่อให้เห็นภาพเท่านั้น

ทำให้ขั้นตอนต่อไปเราเรียกว่า Syntactic Analysis หรือบางคน บางเล่ม ก็จะเรียกว่า Syntax Analysis หรือ Parsing สิ่งที่มันทำ มันจะพยายามสร้างสิ่งที่เรียกว่า Parse Tree

Semantic Analysis

แต่ Tree เฉย ๆ มันก็ยากไปหน่อย ทำให้ขั้นตอนต่อไปคือการทำ Semantic Analysis (ตอนอ่านครั้งแรก ไม่ใช่ Seman-tic แน่ ๆ ถ้าบ้ากามมากพอ) เป็นขั้นตอนของการสร้าง Symbol ละว่าอะไรอยู่ตรงไหน Scope อะไร จากพวกตัวแปร และ Function ต่าง ๆ ที่อยู่ใน Parse Tree สั้น ๆ คือ ทำให้เครื่องรู้ว่า ของชิ้นไหนอยู่ตรงไหนนั่นเอง เวลาจะใช้จะได้หาง่าย

สุดท้ายก็คือการเอา Parse Tree ออกมาค่อย ๆ รันทีละบรรทัด เทียบกับ Table จากขั้นตอนก่อนหน้า เพื่อออกมาสร้างเป็น Machine Code โดยเราเรียกขั้นตอนนี้ว่า Code Generator ตามชื่อเลย

อันนี้ที่เราเอามาเล่าเป็นเพียงขั้นตอนอย่างง่ายของ Compiler เท่านั้นเพื่อให้เข้าใจได้ง่าย เพราะจริง ๆ ข้างใน มันมีอีกหลายเรื่องที่เราไม่ได้พูดถึงอย่าง ขั้นตอน Code Optimisation และพวก Object Linking อีกเยอะ ทำให้ Code ที่ได้ออกมา บางทีมันจะมีอะไรไม่รู้เข้ามา และทำงานแปลก ๆ ไม่เหมือนที่เราคิดซะทีเดียว

Compiler ของ Compiler

อย่างที่เราบอกว่า Compiler เป็นโปรแกรมตัวนึง ซึ่งแน่นอนว่าโปรแกรมตัวนึง การที่มันจะทำงานได้ มันก็ต้องผ่านการแปลงให้กลายเป็น Machine Code ก็ต้องใช้ Compiler ทำให้เรามี Compiler ของ Compiler ต่อกันไปเรื่อย ๆ เมื่อก่อนเราไม่เคยคิดเลยนะเรื่องนี้ จนมานั่งคุยขำ ๆ กับเพื่อน เออ น่าสนใจเฉย

เราเลยลองย้อน ๆ ดูในหลาย ๆ ภาษา บ้างก็เกิดจาก Compiler ของภาษาเดียวกันนี่แหละ แต่เป็น Version ที่เก่ากว่า หรือถ้าเป็น Version แรกของภาษานั้นเลยละ ก็อาจจะเกิดจากภาษาอื่นแบบนี้ลงไปเรื่อย ๆ คือมันลึกมาก ๆ เพราะที่ผ่านมาประวัติศาสตร์ของคอมพิวเตอร์มันเยอะมาก ๆ ไล่หมดคงไม่ไหว ทำให้เกิดคำถามว่า แล้ว Compiler อันแรกเลย มันเกิดขึ้นได้ยังไง ทั้ง ๆ ที่มันไม่มี Compiler เพื่อ Compile มัน

ตรงนี้ เราเดาแล้วนะ ไม่รู้จะหายังไงมาให้เหมือนกัน มี 2 ความเป็นไปได้ อย่างแรกคือ เราก็เขียน Compiler ด้วย Machine Code เลยดิ หรือตอนนั้นอาจจะมีการแปลงอะไรบางอย่างในรูปแบบอื่น เท่าที่จำได้ Compiler อันแรกเลย น่าจะเกิดมาเพื่อ A-0 System นานมาก ๆ แล้ว

CPU ทำงานยังไง ?

Central Processing Unit Structure

อ่านมาถึงตอนนี้ อาจจะสงสัยว่า ทำไม Computer นางเรื่องมากจังอะ ทำไมมันอ่าน Source Code แล้วทำงานเลยไม่ได้ การจะตอบคำถามนี้ได้ เราจะต้องไปทำความเข้าใจหลักการทำงานของ CPU หรือหน่วยประมวลผลกลางกันก่อน

โดยปกติแล้ว สิ่งที่ CPU มันทำได้จริง ๆ แล้ว เบื้องต้น มันจะ คำนวณตัวเลขต่าง ๆ เบื้องต้นได้ เช่น การบวก ลบ คูณ เลข อะไรพวกนั้นผ่านสิ่งที่เรียกว่า ALU (Arithmetic Logic Unit) อีกอย่างที่มันทำได้ก็คือ การเก็บข้อมูลชั่วคราว ผ่าน หน่วยความจำที่เรียกว่า Register

CPU Adding Multiple Numbers

ภาพที่ทำให้เรามองเห็นง่าย ๆ คือ เราอยากจะลองบวกเลข 3 จำนวนเข้าด้วยกัน ตัวอย่างนี้คือ เราบอกว่า ALU ที่มีใน CPU นี้ ทำได้แค่ทีละ 2 จำนวนเท่านั้น ดังนั้น มันก็อาจจะบวก 2 จำนวนแรกเข้าด้วยกัน จากนั้น เอาผลลัพธ์ไปเก็บที่ Register สักตัวนึง และ ดึงอีกจำนวนขึ้นมาบวกกับตัวเลขอีกตัวที่พึ่งเก็บลงไปใน Register ทำให้เราเห็นได้ว่า ทั้ง 2 ส่วนนี้ก็จะทำงานร่วมกัน ถ้าเรามองดี ๆ เห้ย มันก็คือ เครื่องคิดเลขดี ๆ นั่นเอง ก็ตรงกับคำว่า Computer หรือเครื่องคำนวณ หรือ อาจจะกล่าวถึง นักคำนวณในสมัยก่อนก็ได้ (ลองไปดูหนังเรื่อง Hidden Figure ได้ เราเรียกนักคำนวณว่า Computer)

นอกจาก Feature หลัก ๆ ที่มันควรจะมีที่เราเล่าไปแล้ว CPU รุ่นใหม่ ๆ ก็ยังเพิ่มส่วนประกอบอื่น ๆ ลงไปอีก เช่นวงจรสำหรับการ Encode และ Decode Video ต่าง ๆ ที่ทำให้เราเล่นวีดีโอความละเอียดสูงได้ เช่น Intel QuickSync พวกนั้น แต่อันนั้น ก็เป็นเรื่องของแต่ละยี่ห้อ และรุ่นไป แต่ Feature ที่ควรจะมีคือการคำนวณตัวเลข

ในการทำงานจริง ๆ ของ CPU จริง ๆ มันจะมีการกำหนด Instruction หรือสิ่งที่มันทำได้ไว้แล้ว โดยที่คำสั่งเหล่านี้จะถูกทำให้เริ่มทำงานเมื่อเราส่งสัญญาณอย่างถูกต้องเพื่อเรียกมันขึ้นมา เช่น อาจจะส่งสัญญาณว่า 01001 คือการบวกเลขอะไรแบบนั้น พวกสัญญาณเหล่านี้แหละ ทำให้ Transister มันมีการเปิดปิด ทำให้วงจรที่ต่อไว้เล็ก ๆ ทำงาน จนกลายเป็นการคำนวณนั่นเอง (ถ้าอยากรู้ว่าการออกแบบวงจรบวกเลขอะไรง่าย ๆ เบื้องต้นทำยังไง ลองไปหาอ่านหนังสือวิชา Digital System ก็มี) อันนี้คือ Basic นะ ส่วนลึก ๆ ไว้ในโอกาสหน้าละกัน มันยาวมาก

Inside the Machine Code

Example of Machine Code in Binary

ย้อนกลับมาที่เรื่องของ Machine Code ใช่แล้วฮ่ะ มันก็คือชุดคำสั่งที่เราใช้บอก CPU ว่าให้ทำอะไรนั่นเอง ถ้าเราลองเขียน Code และ Compile ออกมา แล้วลองเปิดด้วย Text Editor ออกมา เราจะเห็นว่า มันเป็นอะไรไม่รู้เละ ๆ ไปหมด จริง ๆ แล้วมันต้องเปิดออกมาใน Binary Mode เราก็จะเห็นว่ามันมี 0101 อะไรไม่รู้เต็มไปหมด นี่แหละ คือสิ่งที่เราสั่งให้ CPU ทำงาน โอเค อาจจะอ่านยากไปหน่อย เราแปลงมันให้อยู่ในรูปของ Assembly ละกัน

Example of Assembly Code compiled from gcc
เรา Compile จาก C Source Code ด้วย gcc ถ้าอยากได้เป็น Simplify Version ให้ใส่ -S ตอนที่ Compile เราจะได้ไฟล์แบบนี้ออกมา

จริง ๆ แล้ว ที่เราเห็นเป็น Machine Code เราสามารถทำมันให้ออกมาอยู่ในรูปแบบของ Assembly ได้ เพื่อให้เราเข้าใจได้ง่ายขึ้นตามภาพด้านบนเลย จะเห็นว่า มีบรรทัดที่เรา Highlight ไว้ ถ้าใครที่เคยเขียน Assembly มา เราจะเห็นว่า มันคือคำสั่งสำหรับการย้ายค่าลงไปเก็บใน Memory ในที่นี้เราเก็บค่า 10 ไว้ที่ตำแหน่ง w9

#include <stdio.h>

int main () {
    int a = 10;
    return 0;
}

เมื่อเรามาดู Source Code ต้นฉบับจริง ๆ ก่อนที่จะ Compile จะเห็นได้เลยว่า มันตรงกันเลย คือ เรามีบรรทัดนึง กำหนดให้ a = 10 จากตรงนี้ ทำให้เราเห็นได้ว่า Code ของเรามันผ่านขั้นตอนอย่างไรถึงออกมาเป็น โปรแกรมที่ CPU สามารถทำงานด้วยได้

Flow Control is just HIGH-LEVEL Concept

เวลาเราเขียนโปรแกรม เราอาจจะคุ้นเคยกับ Concept ของ Flow Control ต่าง ๆ เช่น Conditional อย่าง If-Statement หรือ Loop ต่าง ๆ เช่น While และ For เราเขียนมันได้อย่างง่าย ๆ และเข้าใจได้ง่ายมาก ๆ แต่ ไม่ใช่เรื่องง่ายของคอมพิวเตอร์ เพราะมันไม่เข้าใจอะไรแบบนี้ มันเข้าใจแค่ตัวเลขเท่านั้น ดังนั้น มันต้องมีวิธีบางอย่างสำหรับการจัดการกับเรื่องพวกนี้

 #include <stdio.h>
 
 int main () {
     int a = 10;
 
     if (a < 10) {
         a += 1;
     }
     else {
         a -= 1;
     }
 
     return 0;
 }

เราลองเขียนโปรแกรมง่าย ๆ ออกมาละกันเริ่มจากให้ a=10 เหมือนตัวอย่างก่อนหน้าและ มีเงื่อนไขว่า ถ้า a น้อยกว่า 10 ให้บวกเข้าไปอีก 1 หรือ ลบ 1 จาก a ไป

Example of Assembly Code compiled from gcc
เพื่อความเข้าใจง่ายขึ้น ดูที่ตัวเลขประกอบได้

เราลองมาดูใน Assembly กันบ้าง เราเริ่มต้นจากการกำหนดค่า 10 ลงไปใน w8 เหมือนเดิม (1) จากนั้นไปที่ (2) ที่เป็นคำสั่ง CMP หรือก็คือ Compare โดยที่มันให้ Compare ระหว่าง w8 ก็คือ a ที่เราใส่ไปก่อนหน้า กับ ค่าของ 10 เอง จากนั้น ไปที่บรรทัดต่อไป มันใช้คำสั่ง b.ge หbรือก็คือ Branch on Greater than or Equal ก็คือ ให้กระโดด เมื่อมันมากกว่า หรือเท่ากับนั่นเอง

เพราะเรากำหนดว่า ถ้า a น้อยกว่า 10 ให้บวก แต่ใน Compiler เลือกใช้ตรงข้ามกันคือ มากกว่าหรือเท่ากับ ให้กระโดดไปที่ LBB0_2 (4) หรือถ้าไม่ก็ให้ทำงานต่อไปได้เลย ซึ่งเราจะเห็นคำสั่ง ADD อยู่ (3) ก็คือ ให้ เก็บค่าลงไปใน w8 โดยที่ใช้ค่าจาก w8 บวกด้วย 1 ส่วนใน LBB0_2 เราจะเห็นได้ว่า มันใช้คำสั่ง subs (5) หรือ Subtract ก็คือการลบนั่นเอง ก็ให้ลบ 1

#include <stdio.h>
 
 int main () {
     int a = 10;
     for (int i=0; i<5; i++) {
         a += i;
     }
     return 0;
 }

อีกตัวอย่างเพื่อให้สนุกมากขึ้น เรามาลอง Loop กันบ้างดีกว่า เราลองง่าย ๆ คือการเริ่มด้วยประกาศตัวแปร a ให้เท่ากับ 10 เหมือนตัวอย่างก่อน ๆ แต่เราจะเพิ่ม For Loop ขึ้นมาแทน โดยใน Loop เราค่อย ๆ บวก a ไปเรื่อย ๆ ตาม ค่า i ที่เพิ่มขึ้นเรื่อย ๆ จนถึง 4

ดังนั้น การทำงานจริง ๆ ก็คือ เราต้องเริ่มจากกำหนด a เป็น 10 ก่อน ส่วนที่ Tricky น่าจะเป็นส่วนของ Loop นี่ละ เราน่าจะต้องเริ่มจาก การสร้างตัวแปร i และกำหนดให้มันเท่ากับ 0 ก่อน จากนั้น มันก็น่าจะเทียบกับ i ว่าน้อยกว่า 5 มั้ย ถ้าน้อยกว่า ก็ให้ Jump ไปข้างนอกเลย แต่ถ้าไม่ก็ให้เดินต่อโดยให้บวก a ด้วย i และ Jump กลับไปที่ก่อนเช็คเงื่อนไข ก็เป็นอันจบโปรแกรม

Example of Assembly Code compiled from gcc
เพื่อความเข้าใจง่ายขึ้น ดูที่ตัวเลขประกอบได้

ลองมาดูของจริงกันดีกว่า มันจะซับซ้อนกว่าหน่อย โดยเฉพาะส่วนที่เก็บตัวแปร a และ i เพราะมันใช้การ Move ค่าไปมาใน Register ด้วย ทำให้โคตร งง ในตอนแรก โอเค เราเริ่มจากอันแรกคือ เรากำหนด a = 10 ก็คือเหมือนเดิม (1) มัน Move 10 ไปที่ w8 จุดนี้แหละ อ่านดี ๆ นะ มันใช้คำสั่ง str คือ Save ค่าลงไปใน Memory ซึ่งมันเก็บ w8 หรือในตอนนี้คือ a ลงไปใน Memory จำง่าย ๆ ตำแหน่งที่ 8 ละกัน (2) (จริง ๆ มันมีการอ้างอิงอะไรอีก แต่เอาง่ายจำแค่นี้ก่อน) และบรรทัดต่อไปมันเก็บค่าจาก Register wzr ไปใส่ใน Memory ตำแหน่งที่ 4 (3)

จากนั้น ก็จะใช้คำสั่ง LDR ก็คือการโหลดค่าจาก Memory เข้าไปที่ Register ก็คือให้โหลดค่าลง Register w8 จาก Memory ตำแหน่งที่ 4 (4) และเข้า 2 บรรทัดแบบที่เราเคยเห็นกันแล้วคือ CMP ก็คือให้เทียบ w8 (ในที่นี้คือ i) กับ 5 แล้วไปที่ BGE คือถ้ามากกว่า หรือเท่ากับ 5 ให้ Jump ไปที่ Section LBB0_4 หรือก็คือนอก Loop แล้วนั่นเอง

ใน Loop จากเดิม โปรแกรมเราแค่เขียนว่า ให้เอา a บวกด้วย i เท่านั้น แต่เพราะโปรแกรม มันเก็บค่า i และ a ลง Memory ไปแล้ว มันก็ต้องโหลดกลับเข้ามาใหม่ ผ่านคำสั่ง LDR ทั้ง 2 บรรทัด ให้ w8 ไปโหลดตำแหน่งที่ 4 หรือก็คือที่ ๆ เก็บ i ไว้นั่นเอง (5) และอีกบรรทัดก็ทำการโหลดจากตำแหน่งที่ 8 ใน Memory ลง Register w9 (6)

จากนั้น ก็จะ เอา ค่าใน Register w8 และ w9 บวกกัน แล้วใส่ค่าลงไปใน w8 (7) และ ใช้คำสั่ง STR เพื่อโยนค่า w8 หรือผลลัพธ์หลังจากบวกใส่ Memory ตำแหน่งที่ 8 ไปก็คือตำแหน่งของ a นั่นเอง (8)

เกือบจบละ มันก็จะไปที่ Sub-Block bb.3 (9) เริ่มจากโหลดค่า จาก Memory ตำแหน่งที่ 4 ลงไปที่ w8 ซึ่งก็คือค่าของ i นั่นเอง แล้วบวกด้วย 1 และเก็บค่าลงไปที่ตำแหน่งที่ 4 เหมือนเดิม มันก็คือ ส่วนสุดท้ายของ For-Loop ที่เราเขียนว่า i++ นั่นเอง ท้ายสุด มันก็จะเริ่มรอบใหม่ด้วยคำสั่ง B หรือ Jump กลับไปที่ LBB0_1 หรือก็คือ ตอน Loop นั่นเอง

ดังนั้น เราจะเห็นว่า พวก Concept ของ Flow Control จริง ๆ แล้วในเครื่อง มันก็คือการเทียบ และ กระโดดไปมาระหว่างคำสั่งต่าง ๆ และ Concept ของตัวแปรจริง ๆ มันก็คือค่าที่อยู่ใน Memory ซึ่งการจะใช้งานได้มันจะต้องถูกโหลดเข้ามาที่ Register เพื่อให้ CPU สามารถเรียกเข้ามาทำงานได้ (ส่วนพวก HDD และ SSD เป็น Secondary Storage มันมีลำดับขั้นอยู่ลองไปหาอ่านเรื่อง Memory Hierarchy เพิ่มเติมได้)

วิธีนี้มีข้อเสีย

ดูจากวิธีการทำงานคือ เราเขียนโปรแกรม และ Compile ให้อยู่ในรูปแบบของ Machine Code ที่ CPU สามารถอ่านและทำงานได้เลย ดูจะเป็นไอเดียที่เรียบง่าย และ ทำให้โปรแกรมเราทำงานได้เร็วมาก ๆ แต่มันก็มีข้อเสียอยู่

เพราะการที่เราแปลงให้อยู่ใน Machine Code เลย บางทีมันอาจจะมีปัญหาเรื่องความเข้ากันได้ของ สถาปัตยกรรมของ CPU และ OS ที่เราทำงานอยู่ ตัวอย่างเช่น Mac รุ่นใหม่ ๆ ที่เป็น Apple Silicon ไม่สามารถรัน App ที่ทำมาให้ Intel ตรง ๆ ได้ต้องผ่านการแปลง Rosetta เท่านั้น

จากตัวอย่างของ Assembly Code ที่เราเอามาให้ดู เรา Compile ด้วย gcc บน ARM CPU ดังนั้น ถ้าเราลอง Compile ด้วย x86 ก็อาจจะได้ผลลัพธ์ไม่เหมือนกัน ทำให้ใน Assembly คำสั่งเยอะมาก ๆ แยกกันไปตามสถาปัตยกรรมอีกเยอะเกิ๊นนนนน

HandBreak Download Page
Handbreak เองเป็น Open-Source Software สำหรับการแปลงไฟล์ จะเห็นได้ว่าเขาต้อง Compile โปรแกรมออกมาสำหรับหลาย OS หรือแม้กระทั่ง macOS ด้วยกันแยกเป็น Intel กับ Apple Silicon เลย

ทำให้เวลาเราไปดูในหลาย ๆ โปรแกรม เวลาเราจะโหลดมาใช้งาน มันจะเขียนเลยว่า สำหรับ ARM สำหรับ x86_Intel นะ อะไรพวกนั้น ก็คือเป็นเรื่องที่น่าปวดหัวมาก ๆ ยิ่งมีหลายโปรแกรมก็ยิ่งปวดหัวมากเท่านั้น

Interpreter คือคำตอบ ?

Interpreter

เพราะปัญหาที่เราเล่ามา ทำให้มีคนอยากจะแก้ขึ้นมา งั้นเอาใหม่ เราไม่ Compile เป็นอะไรที่เครื่องนั้น ๆ จะเข้าใจละ เราแปลงมันให้อยู่ในรูปแบบนึงละกัน และ เราค่อยใช้โปรแกรมอีกตัวที่เขียนมาเพื่อสถาปัตยกรรม และ OS นั้น ๆ ครอบอีกทีละกัน ทำให้ เวลาเรา Build โปรแกรมออกมา เราก็จะเอาไปรันที่ไหนก็ได้ที่สามารถลง โปรแกรมสำหรับรันได้

ตัวอย่างเช่น Java ที่มันจะไม่แปลงตัวเองเป็น Machine Code เลย แต่มันจะแปลงเป็น Byte Code ถ้าเข้าไปหามันจะเป็นไฟล์ที่นามสกุล .class เมื่อเราจะรัน เราก็จะป้อน Byte Code นี่แหละให้กับ Interpreter ที่จะแปลง Byte Code ให้กลายเป็น Machine Code เพื่อให้เครื่องเข้าใจ ซึ่งโปรแกรมนี้ใน Java เขาเรียกว่า Java Virtual Machine (JVM)

แต่การทำแบบนี้มันก็ต้องมีข้อเสียแน่นอน คือ มันช้า เพราะแทนที่เราจะเอา Machine Code รันได้เลย มันจะต้องโดน Interpreter ครอบ และ แปลงอีกที ทำให้การทำงานส่วนใหญ่จะช้ากว่าพวกภาษาที่ Compile ตรง ๆ มาก ก็ถือว่าเป็น Trade-off กันไป

สรุป

เวลาเราเขียนโปรแกรม และกด Run ดูเหมือนจะไม่มีอะไร แต่ภายในนั้นแฝงไปด้วยกลไกมากมาย เพื่อให้เครื่องคอมพิวเตอร์มันทำงานได้ โดยเฉพาะเรื่องของโปรแกรมที่เป็นเบื้องหลังอย่าง Compiler ที่รับบทนางทาสหนึ่งงง ในการแปลง Source Code ที่เราเข้าใจ ให้กลายเป็น Machine Code ที่เครื่องเข้าใจได้ เราว่ามันเป็นเรื่องที่น่าตื่นเต้นมากเมื่อได้ลองไปอ่านประวัติศาสตร์ของเครื่องคอมพิวเตอร์ ตั้งแต่ต้นจนไปใช้ Punch Card ในการโปรแกรม ยาวจนมาถึงตอนนี้เรามี Compiler, Interpreter และ Debugger รวมไปถึงโปรแกรมต่าง ๆ ล้วนแต่มีที่มาเป็นของตัวเองทั้งนั้น แล้วอนาคตเราจะมีอะไรอีก ก็น่าจะเป็นหน้าที่ของ Computer Scientist แล้วละในการสร้างสิ่งที่น่าตื่นเต้นขึ้นมา