ทำให้แอสเซมเบลอร์“ z80asm” วางคำสั่งในที่อยู่หน่วยความจำที่ทราบ

Dec 30 2020

ฉันกำลังเขียนระบบปฏิบัติการพื้นฐานสำหรับคอมพิวเตอร์ 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เซมเบลอร์วางคำสั่งแรกของรูทีนตามที่อยู่หน่วยความจำที่ฉันตัดสินใจ

คำตอบ

10 Raffzahn Dec 30 2020 at 04:31

อะไรคือวิธีแก้ปัญหาตามรูปแบบบัญญัติสำหรับปัญหาประเภทนี้

ไม่มีโซลูชันที่ยอมรับได้ แต่มีหลายรูปแบบทั้งหมดที่สามารถใช้งานได้

สิ่งเดียวที่อยู่ในใจของฉันคือการสร้าง "ตารางกระโดด" ที่จุดเริ่มต้น

ซึ่งเป็นสิ่งที่ดีที่สมบูรณ์แบบ ยกเว้นโดยปกติจะใช้การกระโดดแทนการเรียกเพื่อลดความยาวของโค้ดเร่งการดำเนินการและลดการโหลดสแต็ก


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 ทำถูกแล้ว

12 WillHartung Dec 30 2020 at 04:55

ปัญหาเกี่ยวกับ 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 มันสมบูรณ์แบบสำหรับสิ่งนั้น

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

5 GeorgePhillips Dec 30 2020 at 03:16

orgสั่งควรทำเฉพาะสิ่งที่คุณถาม อย่างไรก็ตาม z80asm มีความเรียบง่ายเล็กน้อยในรูปแบบผลลัพธ์ แต่คุณสามารถใช้dsเพื่อวางกิจวัตรตามที่อยู่เฉพาะ:

        ds     0x1000
printc:
        ...
        ret

        ds     0x1100-$
readc:
        ...
        ret

ค่านี้จะใส่printcที่ 0x1000 และreadc0x1100 เสมอ มีผลเสียมากมาย ควร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 ตามลำดับ