Skip to content

Instantly share code, notes, and snippets.

@Konamiman
Last active January 26, 2023 16:18
Show Gist options
  • Save Konamiman/384c812c8af4784edfa1c42d4c9c3fb3 to your computer and use it in GitHub Desktop.
Save Konamiman/384c812c8af4784edfa1c42d4c9c3fb3 to your computer and use it in GitHub Desktop.
Parser for Z80 relocatable files (generated by M80.COM, processed by L80.COM)

This program will parse and print the structure of a relocatable (.REL) Z80 code file generated with the Macro80 assembler (M80.COM) and processable with Link80 (L80.COM). See ParseRel.cs for build instructions.

More information on Macro80/Link80 and the .REL format: http://www.msxarchive.nl/pub/msx/programming/asm/m80l80.txt

using System.Diagnostics;
// Adapted from
// https://referencesource.microsoft.com/#PresentationCore/Shared/MS/Internal/Ink/BitStream.cs
namespace Konamiman.ParseRel
{
/// <summary>
/// A stream-style reader for retrieving packed bits from a byte array.
/// Bits are read from the leftmost position in each byte.
/// </summary>
public class BitStreamReader
{
const int BITS_PER_BYTE = 8;
const int BITS_PER_SHORT = 16;
/// <summary>
/// Create a new BitStreamReader to unpack the bits in a buffer of bytes
/// </summary>
/// <param name="buffer">Buffer of bytes</param>
public BitStreamReader(byte[] buffer)
{
Debug.Assert(buffer != null);
_byteArray = buffer;
_bufferLengthInBits = (uint)buffer.Length * (uint)BITS_PER_BYTE;
}
/// <summary>
/// Create a new BitStreamReader to unpack the bits in a buffer of bytes
/// and enforce a maximum buffer read length
/// </summary>
/// <param name="buffer">Buffer of bytes</param>
/// <param name="bufferLengthInBits">Maximum number of bytes to read from the buffer</param>
public BitStreamReader(byte[] buffer, uint bufferLengthInBits)
: this(buffer)
{
if(bufferLengthInBits > (buffer.Length * BITS_PER_BYTE)) {
throw new ArgumentOutOfRangeException("bufferLengthInBits");
}
_bufferLengthInBits = bufferLengthInBits;
}
/// <summary>
/// Read a single UInt16 from the byte[]
/// </summary>
/// <param name="countOfBits"></param>
/// <returns></returns>
public ushort ReadUInt16(int countOfBits)
{
// we only support 1-16 bits currently, not multiple bytes, and not 0 bits
if(countOfBits > BITS_PER_SHORT || countOfBits <= 0) {
throw new ArgumentOutOfRangeException("countOfBits");
}
ushort retVal = 0;
while(countOfBits > 0) {
int countToRead = (int)BITS_PER_BYTE;
if(countOfBits < 8) {
countToRead = countOfBits;
}
//make room
retVal <<= countToRead;
byte b = ReadByte(countToRead);
retVal |= (ushort)b;
countOfBits -= countToRead;
}
return (UInt16)((retVal << 8) + (retVal >> 8));
}
/// <summary>
/// Reads a single bit from the buffer
/// </summary>
/// <returns></returns>
public bool ReadBit()
{
byte b = ReadByte(1);
return ((b & 1) == 1);
}
/// <summary>
/// Read a specified number of bits from the stream into a single byte
/// </summary>
/// <param name="countOfBits">The number of bits to unpack</param>
/// <returns>A single byte that contains up to 8 packed bits</returns>
public byte ReadByte(int countOfBits)
{
// if the end of the stream has been reached, then throw an exception
if(EndOfStream) {
throw new System.IO.EndOfStreamException();
}
// we only support 1-8 bits currently, not multiple bytes, and not 0 bits
if(countOfBits > BITS_PER_BYTE || countOfBits <= 0) {
throw new ArgumentOutOfRangeException("countOfBits");
}
if(countOfBits > _bufferLengthInBits) {
throw new ArgumentOutOfRangeException("countOfBits");
}
_bufferLengthInBits -= (uint)countOfBits;
// initialize return byte to 0 before reading from the cache
byte returnByte = 0;
// if the partial bit cache contains more bits than requested, then read the
// cache only
if(_cbitsInPartialByte >= countOfBits) {
// retrieve the requested count of most significant bits from the cache
// and store them in the least significant positions in the return byte
int rightShiftPartialByteBy = BITS_PER_BYTE - countOfBits;
returnByte = (byte)(_partialByte >> rightShiftPartialByteBy);
// reposition any unused portion of the cache in the most significant part of the bit cache
unchecked // disable overflow checking since we are intentionally throwing away
// the significant bits
{
_partialByte <<= countOfBits;
}
// update the bit count in the cache
_cbitsInPartialByte -= countOfBits;
}
// otherwise, we need to retrieve more full bytes from the stream
else {
// retrieve the next full byte from the stream
byte nextByte = _byteArray[_byteArrayIndex];
_byteArrayIndex++;
//right shift partial byte to get it ready to or with the partial next byte
int rightShiftPartialByteBy = BITS_PER_BYTE - countOfBits;
returnByte = (byte)(_partialByte >> rightShiftPartialByteBy);
// now copy the remaining chunk of the newly retrieved full byte
int rightShiftNextByteBy = Math.Abs((countOfBits - _cbitsInPartialByte) - BITS_PER_BYTE);
returnByte |= (byte)(nextByte >> rightShiftNextByteBy);
// update the partial bit cache with the remainder of the newly retrieved full byte
unchecked // disable overflow checking since we are intentionally throwing away
// the significant bits
{
_partialByte = (byte)(nextByte << (countOfBits - _cbitsInPartialByte));
}
_cbitsInPartialByte = BITS_PER_BYTE - (countOfBits - _cbitsInPartialByte);
}
return returnByte;
}
/// <summary>
/// Since the return value of Read cannot distinguish between valid and invalid
/// data (e.g. 8 bits set), the EndOfStream property detects when there is no more
/// data to read.
/// </summary>
/// <value>True if stream end has been reached</value>
public bool EndOfStream
{
get
{
return 0 == _bufferLengthInBits;
}
}
/// <summary>
/// Advance the stream pointer as needed so it points to the start of the next byte.
/// </summary>
public void ForceByteBoundary()
{
if(_cbitsInPartialByte > 0) ReadByte(_cbitsInPartialByte);
}
// reference to the source byte buffer to read from
private byte[] _byteArray = null;
// maximum length of buffer to read in bits
private uint _bufferLengthInBits = 0;
// the index in the source buffer for the next byte to be read
private int _byteArrayIndex = 0;
// since the bits from multiple inputs can be packed into a single byte
// (e.g. 2 bits per input fits 4 per byte), we use this field as a cache
// of the remaining partial bits.
private byte _partialByte = 0;
// the number of bits (partial byte) left to read in the overlapped byte field
private int _cbitsInPartialByte = 0;
}
}
/*
* To build:
*
* 1. Install the .NET Core 6 SDK, https://dotnet.microsoft.com/en-us/download/dotnet/6.0
*
* 2. From the directory of the files run:
*
* dotnet publish --os=[linux|windows|osx]
*
* 3. Your program is at bin/Release/net6.0/(os)-x64/publish/
*
* Or you can use Visual Studio 2022.
*
* The generated program will require the .NET Runtime 6 to be installed in order to run.
* To avoid this, add '--self-contained=true' to the build command
* (but note that the generated program file will be much bigger).
*/
namespace Konamiman.ParseRel
{
using static Console;
internal class Program
{
static int Main(string[] args)
{
if(args.Length == 0) {
WriteLine(
@"Z80 relocatable file parser 1.0
Bye Konamiman, 2022
Usage: ParseRel <file>"
);
return 0;
}
byte[] bytes;
try {
bytes = File.ReadAllBytes(args[0]);
}
catch(Exception ex) {
Error.WriteLine($"*** Can't read file: {ex.Message}");
return 1;
}
try {
var parser = new RelFileParser(bytes);
parser.ParseFile();
return 0;
}
catch(EndOfStreamException) {
Error.WriteLine("*** Unexpected end of file");
return 2;
}
catch(Exception ex) {
Error.WriteLine($"*** Unexpected error: ({ex.GetType().Name}) {ex.Message}");
return 3;
}
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Configuration>Release</Configuration>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
</Project>
using System.Text;
namespace Konamiman.ParseRel
{
/// <summary>
/// Parser for Microsoft Z80 relocatable files (generated by M80, processed by L80).
/// </summary>
public class RelFileParser
{
const byte ASCII_DOT = 0x2E;
const byte LINK_ITEM_EXTENSION = 4;
const byte LINK_ITEM_PROGRAM_END = 14;
const byte LINK_ITEM_FILE_END = 15;
private string[] addressTypes = {
"", //Absolute
"Code ",
"Data ",
"Common "
};
private string[] linkItemTypes = {
"Entry symbol",
"Select COMMON block",
"Program name",
"Request library search",
"Extension link item",
"Define COMMON size",
"Chain external",
"Define entry point",
"External - offset",
"External + offset",
"Define size of Data area",
"Set loading location counter",
"Chain address",
"Define program size",
"End of program",
"End of file"
};
private string[] arithmeticOperators = {
"Store as byte", //1
"Store as word", //2
"HIGH", //3
"LOW", //4
"NOT", //5
"Unary -", //6
"-", //7
"+", //8
"*", //9
"/", //10
"MOD" //11
};
private byte[] fileContents;
private Action<string> print;
/// <summary>
/// Create a new instance of the class.
/// </summary>
/// <param name="fileContents">File contents to parse.</param>
/// <param name="printer">Delegate to be used to print the generated information, by default it prints to console.</param>
public RelFileParser(byte[] fileContents, Action<string> printer = null)
{
this.fileContents = fileContents;
this.print = printer ?? Print;
}
private List<byte> cummulatedAbsoluteBytes = new List<byte>();
/// <summary>
/// Parse and print the contents of the file.
/// </summary>
public void ParseFile()
{
var bsr = new BitStreamReader(fileContents);
cummulatedAbsoluteBytes.Clear();
while(!bsr.EndOfStream) {
var nextItemIsRelocatable = bsr.ReadBit();
if(!nextItemIsRelocatable) {
var nextAbsoluteByte = bsr.ReadByte(8);
cummulatedAbsoluteBytes.Add(nextAbsoluteByte);
continue;
}
if(cummulatedAbsoluteBytes.Count > 0) {
PrintLine();
foreach(var item in cummulatedAbsoluteBytes) {
print($"{item:X2} ");
}
PrintLine();
PrintLine();
cummulatedAbsoluteBytes.Clear();
}
var relocatableItemType = bsr.ReadByte(2);
if(relocatableItemType != 0) {
var relocatableItem = bsr.ReadUInt16(16);
PrintLine($"{addressTypes[relocatableItemType]}{relocatableItem:X4}");
continue;
}
var linkItemType = bsr.ReadByte(4);
if(linkItemType == LINK_ITEM_FILE_END) {
PrintLine(linkItemTypes[LINK_ITEM_FILE_END]);
return;
}
if(linkItemType == LINK_ITEM_EXTENSION) {
ExtractExtensionLinkItem(bsr);
continue;
}
print($"{linkItemTypes[linkItemType]}");
if(linkItemType >= 5) {
ExtractAItem(bsr);
}
if(linkItemType <= 7) {
ExtractBItem(bsr);
}
if(linkItemType == LINK_ITEM_PROGRAM_END) {
bsr.ForceByteBoundary();
}
PrintLine();
}
}
private void ExtractExtensionLinkItem(BitStreamReader bsr)
{
var specialLintItemSize = bsr.ReadByte(3) - 1;
var specialLintItemType = bsr.ReadByte(8);
var specialItemBytes = new byte[specialLintItemSize];
for(int i = 0; i < specialLintItemSize; i++) {
specialItemBytes[i] = bsr.ReadByte(8);
}
print($"{linkItemTypes[4]}, ");
switch(specialLintItemType) {
case 0x41:
var operatorType = specialItemBytes[0];
var operatorTypeString = operatorType < 1 || operatorType > 11 ? $"{operatorType:X2}" : $"{arithmeticOperators[operatorType - 1]}";
PrintLine($"Arith Operator, {operatorTypeString}");
break;
case 0x42:
PrintLine($"Ref external, {ToAsciiString(specialItemBytes)}");
break;
case 0x43:
var value = specialItemBytes[1] + (specialItemBytes[2] << 8);
PrintLine($"Value, {addressTypes[specialItemBytes[0]]}{value:X4}");
break;
case 0x48:
PrintLine($"Common runtime header, file = {ToAsciiString(specialItemBytes)}");
break;
default:
var specialItemBytesHex = specialItemBytes.Select(b => $"{b:X2}").ToArray();
var toprint = $"{specialLintItemType:X2}, {ToAsciiString(specialItemBytes)} ({string.Join(' ', specialItemBytesHex)})";
PrintLine(toprint);
break;
}
}
private string ToAsciiString(byte[] data)
{
var bytes = data.Select(b => b >= 32 && b < 127 ? b : ASCII_DOT).ToArray();
return Encoding.ASCII.GetString(bytes);
}
private void ExtractAItem(BitStreamReader bsr)
{
var addressType = bsr.ReadByte(2);
var address = bsr.ReadUInt16(16);
print($", {addressTypes[addressType]}{address:X4}");
}
private void ExtractBItem(BitStreamReader bsr)
{
var symbolLength = bsr.ReadByte(3);
var symbolBytes = new byte[symbolLength];
for(int i=0; i<symbolLength; i++) {
symbolBytes[i] = bsr.ReadByte(8);
}
print($", {ToAsciiString(symbolBytes)}");
}
private void Print(string value)
{
Console.Write(value);
}
private void PrintLine(string value = "")
{
print(value);
print(Environment.NewLine);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment