public sealed class X86Disassembler : IDisassembler
{
    public Task<IReadOnlyList<ParsedInstruction>> DisassembleEntryPointAsync(PeImageInfo peInfo, int instructionCount, CancellationToken cancellationToken)
    {
        return Task.FromResult(Disassemble(peInfo, instructionCount));
    }

    private static IReadOnlyList<ParsedInstruction> Disassemble(PeImageInfo peInfo, int instructionCount)
    {
        var instructions = new List<ParsedInstruction>();
        var raw = peInfo.RawBytes;
        var ep = peInfo.NtHeaders.OptionalHeader.AddressOfEntryPoint;
        var epOffset = RvaMapper.RvaToFileOffset(ep, peInfo.Sections, peInfo.NtHeaders.OptionalHeader.SizeOfHeaders);
        if (epOffset is null || epOffset.Value >= raw.Length)
        {
            return instructions;
        }

        var pos = epOffset.Value;
        for (var i = 0; i < instructionCount && pos < raw.Length; i++)
        {
            var opcode = raw[pos];
            if (opcode == 0xC3)
            {
                instructions.Add(new ParsedInstruction((ulong)pos, new[] { opcode }, "ret", string.Empty));
                pos += 1;
                continue;
            }

            if (opcode == 0xE8 && pos + 5 <= raw.Length)
            {
                var bytes = new byte[5];
                Buffer.BlockCopy(raw, pos, bytes, 0, 5);
                instructions.Add(new ParsedInstruction((ulong)pos, bytes, "call", "rel32"));
                pos += 5;
                continue;
            }

            if (opcode == 0xE9 && pos + 5 <= raw.Length)
            {
                var bytes = new byte[5];
                Buffer.BlockCopy(raw, pos, bytes, 0, 5);
                instructions.Add(new ParsedInstruction((ulong)pos, bytes, "jmp", "rel32"));
                pos += 5;
                continue;
            }

            instructions.Add(new ParsedInstruction((ulong)pos, new[] { opcode }, "db", $"0x{opcode:X2}"));
            pos += 1;
        }

        return instructions;
    }
}