เรียนรู้แอสเซมบลี x86-64
(gpfault.net)- บทความแรกของซีรีส์สำหรับผู้เริ่มต้น แอสเซมบลี x86-64
- อธิบายการติดตั้งเครื่องมือและโครงสร้างพื้นฐานโดยอิงจาก ระบบ 64 บิตสมัยใหม่
- แนะนำให้ใช้ Flat Assembler (FASM) และ WinDbg เป็นเครื่องมือหลักสำหรับการพัฒนาและดีบัก
- มีสรุปความรู้สำคัญที่จำเป็นในการใช้งานจริง เช่น ฟอร์แมต PE, การ import DLL, และ calling convention ของ Windows
- อธิบายโดยเน้นการลงมือทำ ตั้งแต่การเขียนโปรแกรมที่จบการทำงานแบบง่าย ๆ ไปจนถึงการฝึก ขั้นตอนการดีบัก
บทนำและความสำคัญ
- ตอนเริ่มเรียน x86 แอสเซมบลีครั้งแรก ในมหาวิทยาลัยมักได้เรียนในสภาพแวดล้อมแบบเก่า เช่น 16 บิต, DOS, และหน่วยความจำแบบเซกเมนต์
- ปัจจุบัน โปรเซสเซอร์ 64 บิต เป็นกระแสหลัก ดังนั้นซีรีส์นี้จะพูดถึงเฉพาะ สภาพแวดล้อม x86-64 ที่ใช้งานจริง และตัดองค์ประกอบแบบเก่าออกทั้งหมด
- บทเรียนนี้มุ่งเน้นการพัฒนาโปรแกรม 64 บิตที่ทำงานบน ระบบปฏิบัติการ Windows
- เริ่มจากโค้ดขนาดเล็กที่สุดที่เข้าถึง OS โดยตรงโดยไม่ใช้ไลบรารี
- บทความนี้เหมาะสำหรับนักพัฒนาที่ต้องการเริ่มเรียนแอสเซมบลี และสมมติว่ามี ความรู้พื้นฐาน C/C++ อยู่แล้ว
การเตรียมเครื่องมือพัฒนา
แอสเซมเบลอร์(Assembler)
- CPU สามารถตีความได้เฉพาะ machine code ซึ่งมนุษย์เข้าใจได้ยาก และแอสเซมบลีภาษาคือสิ่งที่แปลงมันให้อยู่ในรูปของ โค้ดที่มนุษย์อ่านได้
- โปรแกรมที่แปลงภาษาแอสเซมบลีให้เป็น machine code เรียกว่า assembler
- แอสเซมบลี x86-64 ไม่มีมาตรฐานตายตัว แต่ละ assembler จึงมีไวยากรณ์และพฤติกรรมที่ต่างกัน
- ซีรีส์นี้ใช้ Flat Assembler(FASM) ซึ่งมีขนาดเล็ก ใช้งานง่าย และมีระบบมาโครกับเอดิเตอร์ที่ทรงพลัง
ดีบักเกอร์(Debugger)
- ในการวิเคราะห์โค้ดแอสเซมบลีที่เขียนและสังเกตลำดับการทำงาน จำเป็นต้องใช้ debugger เป็นเครื่องมือหลัก
- แนะนำ WinDbg ซึ่งสามารถตรวจดูและแก้ไขรีจิสเตอร์ หน่วยความจำ และโค้ดแอสเซมบลีได้อย่างแยกอิสระ
- สามารถติดตั้งได้โดยเลือกเฉพาะคอมโพเนนต์จาก Windows 10 SDK
- ผ่าน debugger เราสามารถสังเกตสถานะภายในของโปรแกรม โครงสร้างหน่วยความจำ และการเปลี่ยนแปลงของรีจิสเตอร์ได้โดยตรง
มุมมองของการเขียนโปรแกรมแอสเซมบลี
โครงสร้าง CPU และชุดคำสั่ง
- CPU สามารถทำงานได้จำกัดตาม ชุดคำสั่ง ที่กำหนดไว้
- คำสั่ง คือหน่วยงานพื้นฐานที่ CPU สามารถทำได้
- แต่ละคำสั่งทำงานอย่างเรียบง่ายมากพร้อมพารามิเตอร์ เช่น การเก็บค่า หรือการคำนวณเลขคณิต
- สำหรับการเขียนโปรแกรมระดับล่างและการดีบัก หัวใจสำคัญคือการเข้าใจว่าโครงสร้างเหล่านี้เป็นพื้นฐานของแนวคิดระดับสูงทั้งหมด
รีจิสเตอร์(Registers)
- รีจิสเตอร์ คือพื้นที่หน่วยความจำเฉพาะความเร็วสูงมากที่ฝังอยู่ภายใน CPU
- ใน x86-64 มี general-purpose register อยู่ 16 ตัว และทั้งหมดมีขนาด 64 บิต
- แต่ละรีจิสเตอร์สามารถเข้าถึงบางส่วนได้ในระดับไบต์ เวิร์ด และดับเบิลเวิร์ด
| รีจิสเตอร์ | ไบต์ล่าง | เวิร์ดล่าง | ดับเบิลเวิร์ดล่าง |
|---|---|---|---|
| rax | al | ax | eax |
| rbx | bl | bx | ebx |
| rcx | cl | cx | ecx |
| rdx | dl | dx | edx |
| rsp | spl | sp | esp |
| rsi | sil | si | esi |
| rdi | dil | di | edi |
| rbp | bpl | bp | ebp |
| r8~r15 | r8b~r15b | r8w~r15w | r8d~r15d |
rspทำหน้าที่เป็น stack pointer และrsi/rdiทำหน้าที่เป็นดัชนีสำหรับการจัดการสตริง เป็นต้น โดยบางรีจิสเตอร์มี วัตถุประสงค์เฉพาะ กำหนดไว้ripคือ instruction pointer และrflagsคือรีจิสเตอร์พิเศษที่เก็บแฟล็กสถานะของผลลัพธ์จากการคำนวณ
หน่วยความจำและแอดเดรส
- หน่วยความจำทำงานเสมือนอาร์เรย์ของไบต์ที่ต่อเนื่องกันตั้งแต่อินเด็กซ์ 0
- ในสถาปัตยกรรม x86 แบบเก่า วิธี segment-offset เป็นสิ่งจำเป็น แต่ใน x86-64 หน่วยความจำทั้งหมดถูกมองเป็น flat address space
- ในทางปฏิบัติ ระบบปฏิบัติการและฮาร์ดแวร์จะทำการแมป virtual address space ของแต่ละโปรเซสกับหน่วยความจำจริงแบบไดนามิก
- กล่าวคือ แม้จะเป็น virtual address เดียวกัน แต่ในคนละโปรเซสก็อาจอ้างถึงหน่วยความจำจริงคนละตำแหน่ง
- คำสั่งและข้อมูลอยู่ในหน่วยความจำเดียวกัน (สถาปัตยกรรมฟอน นอยมันน์) ซึ่งแตกต่างจาก Harvard architecture ที่เก็บข้อมูลแยกต่างหาก เช่น AVR ที่ใช้กับ Arduino
การเขียนโปรแกรมแอสเซมบลีตัวแรก
- หลังติดตั้ง FASM แล้ว ให้ลองเขียนและ build โปรแกรมง่าย ๆ ด้านล่าง
format PE64 NX GUI 6.0
entry start
section '.text' code readable executable
start:
int3
ret
คำอธิบายโค้ด
format PE64 NX GUI 6.0: ระบุฟอร์แมตไฟล์ปฏิบัติการที่ FASM จะสร้าง ซึ่งในที่นี้คือ PE(Portable Executable) 64 บิต GUIentry start: กำหนด entry point ที่โปรแกรมจะเริ่มทำงาน โดยจะเริ่มรันจากตำแหน่งเลเบล(start)section '.text' code readable executable: ระบุว่าเป็น ส่วนของโค้ด ใน PE และเป็นพื้นที่ที่สามารถรันได้start:: ตั้งชื่อให้จุดเริ่มต้นที่กำหนดไว้ก่อนหน้าint3: breakpoint สำหรับ debugger ใช้หยุดโปรแกรมชั่วคราวเพื่อตรวจสอบสถานะret: คำสั่งที่ดึงแอดเดรสจากสแตกและโอนการควบคุมกลับไปยังตำแหน่งนั้น ซึ่งในโปรแกรมนี้จะตอบสนองด้วยการจบการทำงานทันที
การฝึกดีบัก
-
เปิดไฟล์ปฏิบัติการ(.exe) ของโปรแกรมข้างต้นใน WinDbg แล้วเตรียมหน้าต่างต่าง ๆ เช่น disassembly และ registers
-
กด F5 เพื่อให้โปรแกรมไปถึง breakpoint จากนั้นกด F8 เพื่อรันทีละคำสั่งแบบ step-by-step
-
สามารถสังเกตการเปลี่ยนแปลงของรีจิสเตอร์(
ripเป็นต้น) ได้แบบเรียลไทม์ -
หลัง
retทำงานแล้ว การควบคุมจะถูกส่งกลับไปยังระบบปฏิบัติการ และต่อจากนั้นจะมีการเรียกRtlExitUserThreadเพื่อนำไปสู่การปิด thread และ process -
ข้อควรระวัง : หากจบการทำงานด้วย
retเพียงอย่างเดียว โปรเซสอาจยังคงอยู่ได้ขึ้นกับว่ามีงานเบื้องหลังอื่นกำลังทำงานหรือไม่ ดังนั้น หากต้องการจบอย่างถูกต้อง ควรเรียก ExitProcess เสมอ
ฟอร์แมต PE และการ import DLL
ภาพรวมโครงสร้างการ import ฟังก์ชันจาก DLL
- ฟังก์ชัน WinAPI อย่าง ExitProcess อยู่ใน KERNEL32.DLL
- หากต้องการใช้ฟังก์ชันภายนอกลักษณะนี้ ต้องจัดเตรียม import table ของไฟล์ปฏิบัติการ (ส่วน
.idata) - ใน Import Directory Table(IDT) ของส่วน idata จะมีข้อมูลอย่างชื่อ DLL, ชื่อฟังก์ชัน และแอดเดรส(RVA) ของ IAT/ILT
- IAT(Import Address Table) จะถูกเขียนทับด้วยแอดเดรสจริงของฟังก์ชันโดย OS loader ในช่วงรันไทม์
- Hint/Name Table ประกอบด้วยชื่อและข้อมูล hint ของแต่ละฟังก์ชัน
ตัวอย่างการกำหนดส่วน .idata ใน FASM
section '.idata' import readable writeable
idt:
dd rva kernel32_iat
dd 0
dd 0
dd rva kernel32_name
dd rva kernel32_iat
dd 5 dup(0)
name_table:
_ExitProcess_Name dw 0
db "ExitProcess", 0, 0
kernel32_name: db "KERNEL32.DLL", 0
kernel32_iat:
ExitProcess dq rva _ExitProcess_Name
dq 0
- db/dw/dd/dq : แทรกค่าทีละไบต์/เวิร์ด/ดับเบิลเวิร์ด/ควอดเวิร์ด(8 ไบต์)
- rva : คำนวณ Relative Virtual Address ของสัญลักษณ์
- สามารถอ้างอิงฟังก์ชัน DLL ได้ด้วยการประกอบ IAT และ Name Table แบบทำมือ
Calling Convention ของ Windows 64 บิต(MS x64 Calling Convention)
- เป็นข้อกำหนดมาตรฐานที่กำหนดวิธีส่งอาร์กิวเมนต์และการใช้สแตกระหว่างการเรียกฟังก์ชัน
- ใน Windows 64 บิต จะใช้ Microsoft x64 Calling Convention
- คุณลักษณะสำคัญ :
- stack pointer ต้อง จัดแนว 16 ไบต์ อยู่เสมอ
- อาร์กิวเมนต์จำนวนเต็ม/พอยน์เตอร์ 4 ตัวแรกใช้รีจิสเตอร์ rcx, rdx, r8, r9
- อาร์กิวเมนต์เลขทศนิยม 4 ตัวแรกใส่ไว้ใน xmm0~xmm3
- อาร์กิวเมนต์เพิ่มเติมใช้สแตก
- ต้องสำรอง shadow space 32 ไบต์ บนสแตกโดยไม่ขึ้นกับจำนวนอาร์กิวเมนต์
- ผู้เรียกเป็นผู้จัดการการคืนสภาพสแตก
ตัวอย่างการเรียก ExitProcess
format PE64 NX GUI 6.0
entry start
section '.text' code readable executable
start:
int3
sub rsp, 8 * 5
xor rcx, rcx
call [ExitProcess]
section '.idata' import readable writeable
idt:
dd rva kernel32_iat
dd 0
dd 0
dd rva kernel32_name
dd rva kernel32_iat
dd 5 dup(0)
name_table:
_ExitProcess_Name dw 0
db "ExitProcess", 0, 0
kernel32_name db "KERNEL32.DLL", 0
kernel32_iat:
ExitProcess dq rva _ExitProcess_Name
dq 0
การวิเคราะห์โค้ดใหม่
-
sub rsp, 8 * 5: ปรับ stack pointer (สำรอง 40 ไบต์) เพื่อจัดแนว 16 ไบต์และเตรียม shadow space ในครั้งเดียว -
xor rcx, rcx: กำหนดค่า 0 ให้รีจิสเตอร์ rcx ซึ่งเป็นอาร์กิวเมนต์ตัวแรก (ใช้เป็น EXIT code) -
call [ExitProcess]: กระโดดไปยังแอดเดรสฟังก์ชันของ ExitProcess ที่ถูกบันทึกจริงไว้ใน import table -
เมื่อลองรันทีละขั้นใน WinDbg จะสามารถตรวจสอบการเปลี่ยนแปลงของ stack pointer(
rsp) และ รีจิสเตอร์rcxรวมถึงลำดับการจบของโปรเซสได้โดยตรง
สรุป
- บทความนี้แนะนำภาพรวมของแอสเซมบลี x86-64 โดยเน้นการลงมือทำ ตั้งแต่ การตั้งค่าเครื่องมือพื้นฐาน ฟอร์แมต PE, การ import DLL, x64 calling convention, ไปจนถึงการเขียนและดีบักโปรแกรมแรก
- ในตอนถัดไปจะพูดถึงการสร้างฟังก์ชันที่หลากหลายขึ้นและโค้ดที่ใช้งานจริงมากขึ้น
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
อยากแชร์โปรเจ็กต์ที่พัฒนามาหลายปี
https://asm-editor.specy.app
เป็น IDE แบบอินเทอร์แอ็กทีฟออนไลน์ที่รองรับภาษาแอสเซมบลีหลากหลายแบบ เช่น M68K, MIPS, RISC-V, X86
มีฟีเจอร์มากมายสำหรับใช้สอนการเขียนโปรแกรมแอสเซมบลี
และยังสามารถฝังลงในเว็บไซต์อื่นได้
ไม่เคยรู้มาก่อนว่ารีจิสเตอร์ pointer indexing มีความสามารถในการเข้าถึงไบต์ low address ได้โดยตรง (เช่น ใน 16/32 บิต สามารถเข้าถึง si/esi เป็น sil ได้)
แนวคิดคล้ายกับการเข้าถึง ax/eax เป็น al
เลยสงสัยว่ามี opcode ที่ถูกเพิ่มเข้ามาใหม่ใน x86_64 อยู่จริงหรือไม่
คิดว่าคงต้องกลับไปตรวจดูสเปกของแพลตฟอร์มอีกครั้ง
ถามด้วยความอยากรู้อย่างเดียว
แชร์เอกสารแนะนำแอสเซมบลีสำหรับผู้เริ่มต้นที่เขียนขึ้นเอง
https://www.nayuki.io/page/a-fundamental-introduction-to-x86-assembly-programming
ลองปรับแต่งด้วยแอสเซมบลีเพราะสงสัยว่าจะทำ dispatch ของ CPU emulator ให้เร็วกว่าที่ทำด้วย C++ ได้หรือไม่
ลองรันโปรแกรมฟีโบนักชีแล้ว แต่ผลลัพธ์ก็ยังไม่ใกล้เคียงเลย
สุดท้ายเลยรวมเข้าไปแค่เป็นตัวเลือกปิดใช้งานค่าเริ่มต้น
ถึงอย่างนั้นก็ยังเชื่อว่าต้องมีวิธีทำให้เร็วขึ้นได้อีกแน่
https://github.com/libriscv/libriscv/blob/master/lib/libriscv/amd64/inaccurate_dispatch.nasm
ระหว่างเรียนรู้วิธีเข้าถึงหน่วยความจำก็ปรับปรุงประสิทธิภาพได้เล็กน้อย
ลดขนาด jump table จาก 64 บิตเหลือ 32 บิต และย้ายให้ไปอยู่ในส่วน .text เพื่อให้เข้าถึงแบบ RIP-relative
โปรแกรมฟีโบนักชีไม่ได้ต้องใช้ไบต์โค้ดจำนวนมาก
อยากได้คำแนะนำเรื่องจุดที่ยังปรับปรุงได้จริง ๆ
แม้จะไม่ค่อยรู้บริบท แต่คิดว่าความต่างอาจไม่ได้มาจากกลไก dispatch (วิธี fetch คำสั่ง) แต่อาจมาจากความต่างของการ implement คำสั่งจริง ๆ ก็ได้
วิธีเพิ่มประสิทธิภาพอย่างหนึ่งคือ map รีจิสเตอร์ของเครื่องที่กำลัง emulation ไปยังรีจิสเตอร์จริงของ x86-64 และไม่ปล่อยให้ไหลลงหน่วยความจำเลย
แบบนี้การคำนวณอย่าง add ก็จะทำได้ทันทีโดยไม่ต้องโหลดจากหน่วยความจำ
แต่ข้อเสียคือจะทำให้เขียน emulator ยุ่งยากขึ้นมาก
เป็นเอกสารแนะนำ x86 แอสเซมบลีที่ให้ลองฝึกได้ในเบราว์เซอร์
สามารถรันตัวอย่างได้ทันทีโดยแทบไม่ต้องตั้งค่าอะไรบนเครื่อง
https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html
เพื่อความโปร่งใส ผู้เขียนคือเจ้าตัวเอง
ดูเหมือนเป็นวิธีที่ assemble ด้วย NASM ตรง ๆ แล้วรันไบนารีเลย จึงเริ่มกังวลเรื่องความปลอดภัย
แค่ดูรูปโปรไฟล์ก็นึกว่าเป็น junferno
แค่ได้ลองจับแอสเซมบลีสักครั้งก็ช่วยให้ความเข้าใจภาพรวมลึกขึ้นแล้ว จึงเป็นประสบการณ์ที่ดีเสมอ
ไม่จำเป็นต้องทำเป็นโปรเจ็กต์ใหญ่ แนะนำให้รวบรวมความกล้าลองทำเองสักนิดก็ยังดี
แชร์ลิงก์ไปยังกระทู้ HN ในตอนนั้น (2020)
https://news.ycombinator.com/item?id=24195627
รู้สึกโล่งใจที่ใช้ไวยากรณ์แอสเซมบลีแบบ Intel
อยากลองทำอะไรบางอย่างด้วยแอสเซมบลี แต่ยังคิดไอเดียเฉพาะไม่ออก
เป็นเกมแก้ปริศนาด้วย pseudo-assembly แบบหนึ่ง
คิดว่าเกมแบบนี้น่าจะช่วยตอบสนองความอยากลองแอสเซมบลีได้