ทำให้แอสเซมเบลอร์“ z80asm” วางคำสั่งในที่อยู่หน่วยความจำที่ทราบ
ฉันกำลังเขียนระบบปฏิบัติการพื้นฐานสำหรับคอมพิวเตอร์ homebrew Z80 ของฉัน ในฐานะผู้เริ่มต้นภาษาแอสเซมบลีแบบสมบูรณ์ฉันจึงได้รับ "os plus memory monitor" ที่ใช้งานได้ซึ่งสามารถแสดงเนื้อหาหน่วยความจำและโหลดไบต์ไปยัง RAM ได้ ในการทำเช่นนั้นฉันได้เขียน "กิจวัตรของระบบ" เพื่อเชื่อมต่อกับอุปกรณ์ I / O บางตัว ตัวอย่างเช่นฉันมีรูทีน "Printc" ที่อ่านไบต์และวาดอักขระ ASCII ที่เกี่ยวข้องบนหน้าจอ
สิ่งนี้ใช้ได้กับโค้ดที่สร้างโดยแอสเซมเบลอร์เนื่องจากแอสเซมเบลอร์ตัดสินใจว่าจะใส่ไบต์แรกของรูทีนไว้ที่ใดและใช้แอดเดรสนั้นเมื่อพบคำสั่ง jp ที่มีเลเบลเดียวกัน
ตอนนี้ฉันต้องการเรียกรูทีน Printc จากโปรแกรมที่โหลดแบบไดนามิก ฉันสามารถบอกได้ว่าแอสเซมเบลอร์วางไบต์แรกของรูทีนไว้ที่ใดใน ROM ด้วย-l
แฟล็กที่สร้างเอาต์พุตที่มี:
...
Print: equ $043a Printc: equ $043e
Readc: equ $0442 Readline: equ $0446
...
ตอนนี้ฉันสามารถเขียนโปรแกรมได้ดังนี้:
ld a, 0x50 ; ASCII code for P
call 0x043e ; Calls Printc
โปรแกรมนี้พิมพ์ตัวอักษร P สำเร็จ: ฉันเรียกกิจวัตร Printc ของฉันโดยใช้ที่อยู่หน่วยความจำ
สิ่งนี้ใช้ได้ตราบเท่าที่ฉันไม่ได้เปลี่ยนรหัสแอสเซมบลีใด ๆ ที่นำหน้าการประกาศ Printc ใน "ระบบปฏิบัติการ" ของฉัน ถ้าฉันทำเช่นนั้นป้ายกำกับ Printc จะถูกกำหนดให้กับที่อยู่อื่นและโปรแกรมที่มีอยู่ของฉันจะหยุดทำงาน
อะไรคือวิธีแก้ปัญหาตามรูปแบบบัญญัติสำหรับปัญหาประเภทนี้ สิ่งเดียวที่อยู่ในใจของฉันคือการสร้าง "ตารางกระโดด" ที่จุดเริ่มต้นของรหัสแอสเซมบลีของฉันก่อนที่จะนำเข้าใด ๆ พร้อมกับรายการการเรียกระบบโดยหวังว่าพวกเขาจะได้รับที่อยู่เดียวกันทุกครั้ง สิ่งที่ต้องการ:
...
; System routines
Sys_Print:
call Print
ret
Sys_Printc:
call Printc
ret
.... and so on
แต่ดูเหมือนว่าจะแฮ็คมาก ... เป็นไปได้ไหมที่จะสั่งให้แอสz80asmเซมเบลอร์วางคำสั่งแรกของรูทีนตามที่อยู่หน่วยความจำที่ฉันตัดสินใจ
คำตอบ
อะไรคือวิธีแก้ปัญหาตามรูปแบบบัญญัติสำหรับปัญหาประเภทนี้
ไม่มีโซลูชันที่ยอมรับได้ แต่มีหลายรูปแบบทั้งหมดที่สามารถใช้งานได้
สิ่งเดียวที่อยู่ในใจของฉันคือการสร้าง "ตารางกระโดด" ที่จุดเริ่มต้น
ซึ่งเป็นสิ่งที่ดีที่สมบูรณ์แบบ ยกเว้นโดยปกติจะใช้การกระโดดแทนการเรียกเพื่อลดความยาวของโค้ดเร่งการดำเนินการและลดการโหลดสแต็ก
JUMP_TABLE:
PRINT JP _I_PRINT ; First Function
READC JP _I_READC ; Second Function
...
แต่ดูเหมือนจะแฮ็คมาก ...
ไม่ระบบ 8080 และ Z80 จำนวนมากทำงานเช่นนั้น
ขั้นตอนหลักก่อนหน้านี้คือจุดเข้าทั้งหมดอยู่ที่ตำแหน่งและลำดับที่กำหนดไว้เพียงแห่งเดียว
เป็นไปได้หรือไม่ที่จะสั่งให้แอสเซมเบลอร์ z80asm วางคำสั่งแรกของรูทีนตามที่อยู่หน่วยความจำที่ฉันตัดสินใจ
ได้เลยใช้ ORG วางไว้ตามที่อยู่ที่คุณต้องการ (* 1) แต่นั่นจะเป็นการแฮ็กหรืออย่างน้อยก็ไม่ควรมองไปข้างหน้ามากนัก การมีตารางกระโดดตามที่อยู่ที่กำหนดไว้เป็นการเริ่มต้นที่ดี แน่นอนว่ามันกินพื้นที่ไปบ้าง สามไบต์ต่อรายการ แต่มีเพียงสองรายการเท่านั้นที่เป็นที่อยู่ ทำตารางที่อยู่จะดีกว่าไหม? ชอบ:
SYS_TABLE:
DW _I_PRINT ; First Function
DW _I_READC ; Second Function
การเรียกใช้ฟังก์ชันจะเป็นอย่างไร
LD HL, (SYS_TABLE+0) ; Load Address of First Function - PRINT
JP (HL) ; Do it
สิ่งนี้สามารถใช้ร่วมกับตัวเลือกฟังก์ชันได้อย่างง่ายดาย:
SYS_ENTRY:
PUSH HL
LD H,0
LD L,A
ADD HL,HL
ADD HL,SYS_TABLE
JP (HL)
ตอนนี้แม้แต่ตารางกระโดดก็สามารถย้ายไปมาใน ROM (หรือ RAM) ได้ตามต้องการ
การเรียกใช้หมายเลขฟังก์ชันเช่นเดียวกับระบบปฏิบัติการจำนวนมากเพียงใส่หมายเลขฟังก์ชันใน A แล้วเรียกจุดเริ่มต้นของระบบ (SYS_ENTRY)
LD A,0 ; Print
CALL SYS_ENTRY
แน่นอนว่าจะสามารถอ่านได้มากขึ้นหากระบบปฏิบัติการมีชุดค่าเท่ากับจำนวนฟังก์ชัน :)
จนถึงตอนนี้โปรแกรมที่โหลดยังจำเป็นต้องทราบที่อยู่ตาราง (SYS_TABLE) หรือจุดเริ่มต้นสำหรับตัวเลือก (SYS_ENTRY) ระดับถัดไปของสิ่งที่เป็นนามธรรมจะย้ายที่อยู่ไปยังตำแหน่งที่กำหนดเช่น 0100h ซึ่งดีที่สุดอาจอยู่ในรูปแบบของ JP ดังนั้นโปรแกรมผู้ใช้ใด ๆ จึงเรียกที่อยู่ถาวรนั้น (0100h) เสมอไม่ว่าระบบปฏิบัติการของคุณจะอยู่ใน ROM หรือ RAM หรือที่ใดก็ตาม
และใช่ถ้าสิ่งนี้ดูคุ้นเคยก็เป็นเช่นเดียวกับที่ CP / M จัดการกับการเรียกระบบหรือ MS-DOS
เมื่อพูดถึง MS-DOS จะมีวิธีเพิ่มเติม (และเป็นที่รู้จักกันทั่วไป) ในการเรียกใช้ฟังก์ชัน OS ซึ่งเรียกว่าซอฟต์แวร์ขัดจังหวะเช่น INT 21h ที่รู้จักกันดี และมีบางอย่างที่ค่อนข้างคล้ายกันกับข้อเสนอของ Z80 (และ 8080 ก่อนหน้า): ชุดเวกเตอร์เริ่มต้นใหม่ที่แตกต่างกันแปดตัว (0/8/16 / ... ) การรีสตาร์ท 0 ถูกสงวนไว้สำหรับการรีเซ็ตและสามารถใช้งานอื่น ๆ ทั้งหมดได้ แล้วทำไมไม่ใช้วินาที (RST 8h) สำหรับระบบปฏิบัติการของคุณล่ะ? การเรียกใช้ฟังก์ชันจะมีลักษณะดังนี้:
LD A,0 ; Print
RST 8h
ตอนนี้รหัสโปรแกรมของผู้ใช้แยกออกจากโครงสร้างระบบปฏิบัติการและเค้าโครงหน่วยความจำให้มากที่สุดโดยไม่จำเป็นต้องมีตัวย้ายตำแหน่งหรืออะไรก็ตาม ส่วนที่ดีที่สุดคือเมื่อเล่นซอเล็กน้อยตัวเลือกทั้งหมดจะพอดีกับ 8 ไบต์ที่มีอยู่ทำให้การเข้ารหัสเหมาะสมที่สุด
ข้อเสนอแนะเล็กน้อย:
หากคุณเลือกรุ่นใด ๆ เหล่านี้ตรวจสอบให้แน่ใจว่าฟังก์ชันแรก (0) ของระบบปฏิบัติการของคุณจะเป็นการโทรที่ให้ข้อมูลเกี่ยวกับระบบปฏิบัติการเพื่อให้โปรแกรมต่างๆสามารถตรวจสอบความเข้ากันได้ ควรส่งคืนค่าพื้นฐานอย่างน้อยสองค่า:
- หมายเลขรุ่น ABI
- จำนวนฟังก์ชันที่รองรับสูงสุด
ABIจำนวนการเปิดตัวอาจจะหรืออาจจะไม่เหมือนกับหมายเลขรุ่น แต่ไม่ได้มีการ จะต้องเพิ่มขึ้นทุกครั้งที่มีการเปลี่ยนแปลง API เมื่อใช้ร่วมกับหมายเลขฟังก์ชันที่รองรับสูงสุดโปรแกรมผู้ใช้สามารถใช้ข้อมูลนี้เพื่อเลิกใช้งานได้อย่างสง่างามในกรณีที่เข้ากันไม่ได้ - แทนที่จะหยุดทำงานกลางคัน เพื่อความหรูหราฟังก์ชันอาจส่งคืนตัวชี้ไปยังไฟล์
- โครงสร้างที่มีข้อมูลเพิ่มเติมเกี่ยวกับระบบปฏิบัติการเช่น
- ชื่อ / เวอร์ชันที่อ่านได้
- ที่อยู่ของแหล่งต่างๆ
- จุดเข้า "พิเศษ"
- ข้อมูลเครื่องเช่นขนาด RAM
- อินเทอร์เฟซที่ใช้ได้ ฯลฯ
แค่พูด...
* 1 - และไม่นอกเหนือจากที่บางคนอาจคิดว่า ORG ไม่ควรเพิ่มช่องว่างภายในหรือเหมือนกัน ผู้ประกอบการทำเช่นนั้นเป็นทางเลือกที่ไม่ดี องค์กรควรเปลี่ยนระดับที่อยู่เท่านั้นอย่ากำหนดสิ่งที่อยู่ในพื้นที่ใด ๆ ที่ "กระโดดข้าม" การทำเช่นนั้นอาจเพิ่มระดับของข้อผิดพลาดที่อาจเกิดขึ้นได้อย่างน้อยก็ทันทีที่การใช้งาน ORG ขั้นสูงบางอย่างเสร็จสิ้นเชื่อฉันเถอะ ORG เป็นเครื่องมือที่หลากหลายมากเมื่อทำโครงสร้างที่ซับซ้อน
นอกจากนี้การเติมพื้นที่ 'ว่างเปล่า' ด้วยช่องว่างภายในบางส่วนจะส่งผลให้ช่องว่างภายในนี้เป็นส่วนหนึ่งของโปรแกรมแทนที่จะเป็นหน่วยความจำที่ไม่ถูกแตะต้องทำให้เครื่องมือหลักสำหรับการแก้ไขในภายหลัง: พื้นที่ EPROM ที่ไม่ได้กำหนดค่าเริ่มต้น เพียงแค่ไม่กำหนดและไม่โหลดพื้นที่เหล่านี้พื้นที่เหล่านี้จะยังคงอยู่ในสถานะที่เคลียร์ (ทั้งหมดในกรณีของ EPROM) และสามารถตั้งโปรแกรมได้ในภายหลัง - ตัวอย่างเช่นเพื่อเก็บโค้ดไว้ระหว่างการดีบักหรือเพื่อใช้การแก้ไขด่วนโดยไม่ต้อง ความต้องการในการเขียนโปรแกรมอุปกรณ์ใหม่
ดังนั้นหน่วยความจำที่ไม่ได้กำหนดควรเป็นเพียงแค่นั้นไม่ได้กำหนด และนั่นเป็นเหตุผลว่าทำไมรูปแบบเอาต์พุต / ตัวโหลดของแอสเซมเบลอร์ที่เก่าแก่ที่สุด (เช่นMotorola SRECหรือIntel HEX ) ที่ใช้สำหรับการจัดส่งโปรแกรมไปยังทุกอย่างตั้งแต่การสร้าง ROM จนถึงโปรแกรมผู้ใช้ที่รองรับวิธีการออกจากพื้นที่
เรื่องสั้นขนาดยาว: ถ้าใครอยากเติมก็ต้องทำ expcit z80asm ทำถูกแล้ว
ปัญหาเกี่ยวกับ Z80ASM โดยเฉพาะคือใช้อินพุตแอสเซมบลีและคายไฟล์ไบนารีแบบคงที่ นี่คือสิ่งที่ดีและไม่ดี
ในระบบ "ปกติ" การกำหนดแอดเดรสเป็นความรับผิดชอบของผู้เชื่อมโยงไม่ใช่ผู้ประกอบอย่างหลีกเลี่ยงไม่ได้ แต่แอสเซมเบลอร์นั้นง่ายพอที่หลาย ๆ คนจะข้ามแง่มุมนั้นของวงจรการสร้างไป
เนื่องจาก Z80ASM คายภาพไบนารีตามตัวอักษรออกมาแทนที่จะเป็นไฟล์ "วัตถุ" จึงไม่จำเป็นต้องมีตัวเชื่อมโยง แต่มันก็ไม่ยอมให้คุณทำในสิ่งที่คุณต้องการทำเสมอไป
พิจารณาคำสั่ง ORG ที่แพร่หลาย
ORG บอกแอสเซมเบลอร์ว่าที่อยู่เริ่มต้น (ต้นทาง - ดังนั้น ORG) คืออะไรสำหรับรหัสแอสเซมบลีที่จะเกิดขึ้น
ซึ่งหมายความว่าหากคุณทำสิ่งนี้:
ORG 0x100
L1: jp L1
แอสเซมเบลอร์จะประกอบคำสั่ง JP เข้ากับ JUMP ไปยังแอดเดรส 0x100 (L1)
แต่เมื่อมันคายไฟล์ไบนารีออกมาไฟล์จะมีขนาดเพียง 3 ไบต์ คำสั่งกระโดดตามด้วย 0x100 ในรูปแบบไบนารี ไม่มีอะไรในไฟล์นี้ที่บอกอะไรเลยว่าต้องโหลดที่ 0x100 ถึงจะ "ทำงาน" ได้ ข้อมูลนั้นหายไป
ถ้าคุณทำ:
ORG 0x100
L1: jp L2
ORG 0x200
L2: jp L1
สิ่งนี้จะสร้างไฟล์ที่มีความยาว 6 ไบต์ มันจะใส่คำสั่ง JP สองคำนี้ต่อกัน สิ่งเดียวที่คำสั่ง ORG กำลังทำคือการบอกว่าฉลากควรเป็นอย่างไร นี่ไม่ใช่สิ่งที่คุณคาดหวัง
ดังนั้นการเพิ่ม ORG ลงในไฟล์ของคุณจะไม่ทำในสิ่งที่คุณต้องการเว้นแต่คุณจะมีวิธีอื่นในการโหลดโค้ดในตำแหน่งเฉพาะที่คุณต้องการให้โค้ดของคุณเป็น
วิธีเดียวที่จะทำเช่นนั้นกับ Z80ASM นอกกรอบคือการเติมไฟล์เอาต์พุตของคุณด้วยบล็อกไบต์พื้นที่ว่างซึ่งจะเติมไบนารีเพื่อวางโค้ดของคุณในตำแหน่งที่ถูกต้อง
โดยปกตินี่คือสิ่งที่ผู้เชื่อมโยงทำเพื่อคุณ หน้าที่ของผู้เชื่อมโยงคือการใช้รหัสที่แตกต่างกันของคุณและสร้างภาพไบนารีที่เป็นผลลัพธ์ ทั้งหมดนี้ทำเพื่อคุณ
ในแอสเซมเบลอร์ของฉันซึ่งไม่ได้ใช้ตัวเชื่อมโยงมันสร้างรูปแบบไฟล์ Intel HEX ซึ่งรวมถึงที่อยู่จริงสำหรับแต่ละบล็อกของข้อมูล
ดังนั้นสำหรับตัวอย่างก่อนหน้านี้จะมีการสร้างสองระเบียน หนึ่งกำหนดไว้สำหรับ 0x100 อีกตัวหนึ่งสำหรับ 0x200 จากนั้นโปรแกรมโหลดฐานสิบหกจะวางสิ่งต่างๆไว้ในตำแหน่งที่ถูกต้อง นี่เป็นอีกทางเลือกหนึ่ง แต่ Z80ASM ดูเหมือนจะไม่รองรับเช่นกัน
ดังนั้น.
Z80ASM นั้นยอดเยี่ยมมากหากคุณสร้างภาพ ROM โดยเริ่มจากพูดโดยพลการ 0x1000 คุณจะ ORG นั้นรับไบนารีที่เป็นผลลัพธ์และดาวน์โหลดไฟล์ทั้งหมดที่เบิร์นใน EPROM มันสมบูรณ์แบบสำหรับสิ่งนั้น
แต่สำหรับสิ่งที่คุณต้องการทำคุณจะต้องวางรหัสเพื่อย้ายกิจวัตรของคุณไปยังที่ที่ถูกต้องหรือคิดแผนตัวโหลดอื่น ๆ เพื่อแสดงให้คุณเห็น
org
สั่งควรทำเฉพาะสิ่งที่คุณถาม อย่างไรก็ตาม z80asm มีความเรียบง่ายเล็กน้อยในรูปแบบผลลัพธ์ แต่คุณสามารถใช้ds
เพื่อวางกิจวัตรตามที่อยู่เฉพาะ:
ds 0x1000
printc:
...
ret
ds 0x1100-$
readc:
...
ret
ค่านี้จะใส่printc
ที่ 0x1000 และreadc
0x1100 เสมอ มีผลเสียมากมาย ควรprintc
มีขนาดใหญ่กว่า 0x100 โปรแกรมจะไม่รวมตัวกันและคุณจะต้องแยกตัวprintc
ออกจากกันในบางรูปแบบและใส่รหัสพิเศษไว้ที่อื่น ด้วยเหตุนี้และเหตุผลอื่น ๆ ตารางกระโดดที่ตำแหน่งคงที่ในหน่วยความจำจึงจัดการได้ง่ายกว่าและมีความยืดหยุ่นมากขึ้น:
ds 0x100
v_printc: jp printc
v_readc: jp readc
...
อีกเทคนิคหนึ่งคือการใช้จุดเข้าเพียงจุดเดียวและเลือกฟังก์ชันโดยใช้ค่าในการA
ลงทะเบียน อย่างน้อยจะช้าลงเล็กน้อย แต่หมายความว่าต้องมีการรักษาจุดเข้าใช้งานเพียงจุดเดียวเมื่อระบบปฏิบัติการเปลี่ยนแปลง
และแทนที่จะทำCALL
จุดเริ่มต้นให้วางไว้ที่ตำแหน่งพิเศษแห่งใดแห่งหนึ่งRST
(0, 8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38) ซึ่งคุณสามารถใช้RST 0x18
เป็นการเรียกแบบไบต์เดียวไปยังตำแหน่งหน่วยความจำ 0x18 โดยปกติRST 0
และRST 0x38
จะหลีกเลี่ยงเนื่องจากเป็นจุดเริ่มต้นของ pwoer-on และสถานที่จัดการขัดจังหวะรุ่น 1 ตามลำดับ