ขั้นตอนการทำงานของโปรแกรมยูบูต (u-boot execution flow)

By | November 29, 2016

หลังจากที่ได้ทำความเข้าใจกับขั้นตอนการสร้างยูบูต (compile-time) แล้ว เราลองมาทำความเข้าใจกับขั้นตอนการทำงานของโปรแกรมยูบูต (run-time) กันต่อเลยดีกว่า

โดยทั่วไปสามารถแบ่งขั้นตอนการทำงานของโปรแกรมยูบูต ได้เป็นสองรูปแบบ คือ แบบมีการย้ายตำแหน่งของยูบูต (Relocation) กับแบบไม่มีการย้ายตำแหน่งของยูบูต (Non relocation) ซึ่งมีขั้นตอนโดยรวมแทบจะเหมือนกันเว้นแต่ส่วนการย้ายยูบูตไบนารี่โค้ด แม้ว่าในโค้ดเวอร์ชั่นปัจจุบัน (2016.xx) จะใช้การย้ายตำแหน่งของยูบูต (Relocation) เป็นส่วนใหญ่ แต่เรายังสามารถพบลักษณะการบูตโดยไม่มีการย้ายตำแหน่งของยูบูต ในระบบขนาดเล็ก เนื่องการทำงานของโค้ดส่วนนี้ มีความน่าสนใจศึกษาอยู่ ในบทความนี้จึงขอแบ่งรูปแบบของการทำงานโดยใช้เรื่องนี้เป็นหลักนะครับ

ก่อนจะทำความเข้าใจการทำงานทั้งหมด ขออธิบายเรื่องการย้ายตำแหน่งยูบูตให้เข้าใจกันก่อน

ความหมายและความจำเป็นของการย้ายตำแหน่งยูบูต

การย้ายตำแหน่งของยูบูต (u-boot relocation) คือขั้นตอนในการคัดลอกรหัสคำสั่งของยูบูตจากแฟลซเมมโมรี่หรือเมมโมรี่ ไปยังเมมโมรี่ตำแหน่งอื่นในระหว่างขั้นตอนการเริ่มการทำงานของยูบูต เนื่องจากตัวแปรขณะเวลาทำงานบางตัวจำเป็นต้องมีการเปลี่ยนแปลงค่าได้ขณะทำงาน (writable) ซึ่งการกำหนดค่าตำแหน่งต่างๆให้กับตัวแปรเหล่านี้จะทำได้ง่ายและยืดหยุ่นกว่ามากหากตัวโปรแกรมทำงานอยู่บนเมมโมรี่ และอีกเหตุผลหนึ่งคือทำให้ตัวยูบูตสามารถอัพเดทตัวเองลงแฟลซเมมโมรี่ได้ขณะทำงาน ซึ่งจะทำไม่ได้เลยหากโปรแกรมยังรันอยู่ในตำแหน่งเดียวกันบนแฟลซเนื่องจากเป็นการทำลายตัวเอง (self destruction)

ในช่วงแรกของโปรเจคยูบูต การย้ายตำแหน่งของยูบูต (u-boot relocation) ยังไม่มีความจำเป็นมากเท่าไรนัก เนื่องจากยูบูตทำงานเพียงแค่เป็นบูตโหลดเดอร์เท่านั้น (หมายความว่าแค่คัดลอกตัวเคอร์เนลไปยังเมมโมรี่และสตาร์ทได้ก็จบ) แต่เมื่อตัวโปรเจคมีความสามารถมากขึ้น การใช้งานหน่วยความจำก็เริ่มจำเป็นต้องมีความเป็นไดนามิกมากขึ้น ทำให้นักพัฒนาพยายามเพิ่มโค้ดส่วนย้ายตำแหน่งของยูบูตไปยังเมมโมรี่หลักเพื่อที่จะได้ง่ายต่อการพัฒนาและรองรับความสามารถที่ยืดหยุ่นนี้

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

ขั้นตอนการทำงานของโค้ดส่วนย้ายตำแหน่งของยูบูต

การทำงานของโค้ดส่วนย้ายตำแหน่งของยูบูตอาจแตกต่างไปจากรูปแบบในตัวอย่างที่นำมาให้ศึกษาด้านล่างได้ เนื่องจากความสามารถและข้อจำกัดของแต่ละสถาปัตยกรรมของซีพียู แต่หลักการทั่วไปจะเหมือนคือการคำนวนหาตำแหน่งใหม่ของ ยูบูต, โกลบอลดาต้า(GD) และสแตก พอยเตอร์(SP) จากนั้นคัดลอกตัวยูบูตไบนารี่ทั้งหมด, โกลบอลดาต้า(GD) และสแตก พอยเตอร์(SP) ไปยังเมมโมรี่ตำแหน่งใหม่ รวมถึงและทำการเปลื่ยนค่าอ้างอิงต่างๆให้สอดคล้องกัน จากนั้นจึงเปลื่ยนค่าโปรแกรมเคาน์เตอร์(PC) เพิ่มให้ซีพียูกระโดดไปทำงานในตำแหน่งใหม่ ซึ่งส่วนใหญ่จะเป็นตำแหน่งท้ายๆของ เมมโมรี่สเปซ เนื่องจากต้องการให้พื้นที่ของเมมโมรี่ที่จะส่งมอบให้กับเคอร์เนลมีความต่อเนื่องให้มากที่สุด (Contiguous Memory Space)

ubootrelocate_w

ในบางสถาปัตยกรรมของซีพียูที่รองรับการเปลื่ยนแปลงค่าของการวางแบบของหน่วยความจำ (Memory layout) ขณะทำงานโค้ดส่วนย้ายตำแหน่งของยูบูตจะไม่ต้องทำการเปลื่ยนค่าอ้างอิงต่างๆให้สอดคล้องกับค่าตำแหน่งใหม่ เนื่องจากสามารถสลับตำแหน่งของแฟลซเมมโมรี่กับเมมโมรี่ได้ทันที ทำให้เสมือนว่าซีพียูยังทำงานต่อเนื่องที่ตำแหน่งเดิม ความสามารถนี้เรียกว่า “Memory Remap”

ubootremap_w

ขั้นตอนการเปลื่ยนแปลงค่าของการวางแบบของหน่วยความจำนี้ (Memory Remap) เป็นเพียงแค่ทางเลือกสำหรับนักพัฒนา อาจมีหรือไม่มี หรืออาจใช้ร่วมกับการย้ายตำแหน่งของยูบูต(Relocation) ขึ้นอยู่กับการออกแบบ

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

ขั้นตอนการทำงานโดยรวมทั้งหมดของยูบูต (u-boot execution flow)

ขั้นตอนการทำงานของโปรแกรมยูบูตสามารถแสดงได้ดังรูป

u-boot run time

เริ่มจากเมื่อบอร์ดถูกรีเซ็ต ซีพียูจะกระโดดไปทำงานที่ตำแหน่ง reset vector ซึ่งเป็นตำแหน่งของฟังก์ชั่น _start() จากไฟล์ start.S จากนั้นจะเรียก lowlevel_init() จากไฟล์ lowlevel_init.S ให้ทำการกำหนดค่าขั้นต่ำเพื่อให้ c-code สามารถเริ่มทำงานได้

จากนั้น board_init_f() จะถูกเรียกมาทำงานขั้นต่อไปคือการตั้งค่าส่วนควบคุมหน่วยความจำหลักและพอร์ตอนุกรม ตัวแปร global_data และหน่วยความจำสแตกจะถูกสร้างในขั้นตอนนี้ ส่วนตัวแปรโกลบอลและตัวแปรสแตติกจะยังไม่สามารถใช้ได้เนื่องจากหน่วยความจำเซ็กเม้น .bss ยังไม่มี (ตัวแปร global_data เป็นชื่อตัวแปร ไม่ได้เกี่ยวของกับตัวแปรโกลบอล อย่าสับสนนะครับ )
dram_init() จะถูกเรียกเพื่อตั้งค่าการทำงานให้กับ DRAM แต่ถ้าหากมีการตั้งค่ามาแล้วจากบูตโหลดเดอร์สเตทก่อนหน้า ขั้นตอนนี้จะถูกข้ามไป หลังจากนี้ขั้นตอนการย้ายตำแหน่งของยูบูตที่อธิบายข้างต้นจะเกิดขึ้น

board_init_r() จะทำงานตั้งค่าต่างๆของบอร์ด ในขั้นตอนนี้หน่วยความจำเซ็กเม้น .bss พร้อมใช้งานแล้ว ทำให้การสร้างตัวแปรต่างๆ ทำได้อย่างสมบูรณ์ จากนั้นโปรแกรมจะเข้าสู่ลูปหลัก เพื่อมอนิเตอร์คำสั่งที่ป้อนมาจากผู้ใช้งานทางคอนโซลพอร์ตอนุกรม หรือ ทำหน้าที่โหลดเคอร์เนลไบนารี่ตามที่ระบุไว้ใน ตัวแปร $bootcmd

ขั้นตอนหลักทั่วไปเป็นดังนี้ เห็นได้ว่ายูบูตไม่ได้มีการทำงานที่ซับซ้อนมากมายอะไร เนื่องจากตัวซีพียูยังทำงานในโหมด single thread และขั้นตอนการทำงานค่อนข้างเป็นเส้นตรง จึงเป็นตัวอย่างที่ดีสำหรับคนที่สนใจการทำงานของระบบสมองกลฝังตัว

ในรายละเอียดของส่วนอื่นผมจะพยายามจะอธิบายในบทต่อๆไปนะครับ