bgfx/3rdparty/spirv-tools/source/disassemble.cpp
Бранимир Караџић 4157813255 Updated spirv-tools.
2024-10-18 20:28:32 -07:00

1110 lines
40 KiB
C++

// Copyright (c) 2015-2020 The Khronos Group Inc.
// Modifications Copyright (C) 2020 Advanced Micro Devices, Inc. All rights
// reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file contains a disassembler: It converts a SPIR-V binary
// to text.
#include "source/disassemble.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <iomanip>
#include <memory>
#include <set>
#include <stack>
#include <unordered_map>
#include <utility>
#include "source/assembly_grammar.h"
#include "source/binary.h"
#include "source/diagnostic.h"
#include "source/ext_inst.h"
#include "source/opcode.h"
#include "source/parsed_operand.h"
#include "source/print.h"
#include "source/spirv_constant.h"
#include "source/spirv_endian.h"
#include "source/util/hex_float.h"
#include "source/util/make_unique.h"
#include "spirv-tools/libspirv.h"
namespace spvtools {
namespace {
// Indices to ControlFlowGraph's list of blocks from one block to its successors
struct BlockSuccessors {
// Merge block in OpLoopMerge and OpSelectionMerge
uint32_t merge_block_id = 0;
// The continue block in OpLoopMerge
uint32_t continue_block_id = 0;
// The true and false blocks in OpBranchConditional
uint32_t true_block_id = 0;
uint32_t false_block_id = 0;
// The body block of a loop, as specified by OpBranch after a merge
// instruction
uint32_t body_block_id = 0;
// The same-nesting-level block that follows this one, indicated by an
// OpBranch with no merge instruction.
uint32_t next_block_id = 0;
// The cases (including default) of an OpSwitch
std::vector<uint32_t> case_block_ids;
};
class ParsedInstruction {
public:
ParsedInstruction(const spv_parsed_instruction_t* instruction) {
// Make a copy of the parsed instruction, including stable memory for its
// operands.
instruction_ = *instruction;
operands_ =
std::make_unique<spv_parsed_operand_t[]>(instruction->num_operands);
memcpy(operands_.get(), instruction->operands,
instruction->num_operands * sizeof(*instruction->operands));
instruction_.operands = operands_.get();
}
const spv_parsed_instruction_t* get() const { return &instruction_; }
private:
spv_parsed_instruction_t instruction_;
std::unique_ptr<spv_parsed_operand_t[]> operands_;
};
// One block in the CFG
struct SingleBlock {
// The byte offset in the SPIR-V where the block starts. Used for printing in
// a comment.
size_t byte_offset;
// Block instructions
std::vector<ParsedInstruction> instructions;
// Successors of this block
BlockSuccessors successors;
// The nesting level for this block.
uint32_t nest_level = 0;
bool nest_level_assigned = false;
// Whether the block was reachable
bool reachable = false;
};
// CFG for one function
struct ControlFlowGraph {
std::vector<SingleBlock> blocks;
};
// A Disassembler instance converts a SPIR-V binary to its assembly
// representation.
class Disassembler {
public:
Disassembler(const AssemblyGrammar& grammar, uint32_t options,
NameMapper name_mapper)
: print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
nested_indent_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT, options)),
reorder_blocks_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_REORDER_BLOCKS, options)),
text_(),
out_(print_ ? out_stream() : out_stream(text_)),
instruction_disassembler_(grammar, out_.get(), options, name_mapper),
header_(!spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NO_HEADER, options)),
byte_offset_(0) {}
// Emits the assembly header for the module, and sets up internal state
// so subsequent callbacks can handle the cases where the entire module
// is either big-endian or little-endian.
spv_result_t HandleHeader(spv_endianness_t endian, uint32_t version,
uint32_t generator, uint32_t id_bound,
uint32_t schema);
// Emits the assembly text for the given instruction.
spv_result_t HandleInstruction(const spv_parsed_instruction_t& inst);
// If not printing, populates text_result with the accumulated text.
// Returns SPV_SUCCESS on success.
spv_result_t SaveTextResult(spv_text* text_result) const;
private:
void EmitCFG();
const bool print_; // Should we also print to the standard output stream?
const bool nested_indent_; // Should the blocks be indented according to the
// control flow structure?
const bool
reorder_blocks_; // Should the blocks be reordered for readability?
spv_endianness_t endian_; // The detected endianness of the binary.
std::stringstream text_; // Captures the text, if not printing.
out_stream out_; // The Output stream. Either to text_ or standard output.
disassemble::InstructionDisassembler instruction_disassembler_;
const bool header_; // Should we output header as the leading comment?
size_t byte_offset_; // The number of bytes processed so far.
bool inserted_decoration_space_ = false;
bool inserted_debug_space_ = false;
bool inserted_type_space_ = false;
// The CFG for the current function
ControlFlowGraph current_function_cfg_;
};
spv_result_t Disassembler::HandleHeader(spv_endianness_t endian,
uint32_t version, uint32_t generator,
uint32_t id_bound, uint32_t schema) {
endian_ = endian;
if (header_) {
instruction_disassembler_.EmitHeaderSpirv();
instruction_disassembler_.EmitHeaderVersion(version);
instruction_disassembler_.EmitHeaderGenerator(generator);
instruction_disassembler_.EmitHeaderIdBound(id_bound);
instruction_disassembler_.EmitHeaderSchema(schema);
}
byte_offset_ = SPV_INDEX_INSTRUCTION * sizeof(uint32_t);
return SPV_SUCCESS;
}
spv_result_t Disassembler::HandleInstruction(
const spv_parsed_instruction_t& inst) {
instruction_disassembler_.EmitSectionComment(inst, inserted_decoration_space_,
inserted_debug_space_,
inserted_type_space_);
// When nesting needs to be calculated or when the blocks are reordered, we
// have to have the full picture of the CFG first. Defer processing of the
// instructions until the entire function is visited. This is not done
// without those options (even if simpler) to improve debuggability; for
// example to be able to see whatever is parsed so far even if there is a
// parse error.
if (nested_indent_ || reorder_blocks_) {
switch (static_cast<spv::Op>(inst.opcode)) {
case spv::Op::OpLabel: {
// Add a new block to the CFG
SingleBlock new_block;
new_block.byte_offset = byte_offset_;
new_block.instructions.emplace_back(&inst);
current_function_cfg_.blocks.push_back(std::move(new_block));
break;
}
case spv::Op::OpFunctionEnd:
// Process the CFG and output the instructions
EmitCFG();
// Output OpFunctionEnd itself too
[[fallthrough]];
default:
if (!current_function_cfg_.blocks.empty()) {
// If in a function, stash the instruction for later.
current_function_cfg_.blocks.back().instructions.emplace_back(&inst);
} else {
// Otherwise emit the instruction right away.
instruction_disassembler_.EmitInstruction(inst, byte_offset_);
}
break;
}
} else {
instruction_disassembler_.EmitInstruction(inst, byte_offset_);
}
byte_offset_ += inst.num_words * sizeof(uint32_t);
return SPV_SUCCESS;
}
// Helper to get the operand of an instruction as an id.
uint32_t GetOperand(const spv_parsed_instruction_t* instruction,
uint32_t operand) {
return instruction->words[instruction->operands[operand].offset];
}
std::unordered_map<uint32_t, uint32_t> BuildControlFlowGraph(
ControlFlowGraph& cfg) {
std::unordered_map<uint32_t, uint32_t> id_to_index;
for (size_t index = 0; index < cfg.blocks.size(); ++index) {
SingleBlock& block = cfg.blocks[index];
// For future use, build the ID->index map
assert(static_cast<spv::Op>(block.instructions[0].get()->opcode) ==
spv::Op::OpLabel);
const uint32_t id = block.instructions[0].get()->result_id;
id_to_index[id] = static_cast<uint32_t>(index);
// Look for a merge instruction first. The function of OpBranch depends on
// that.
if (block.instructions.size() >= 3) {
const spv_parsed_instruction_t* maybe_merge =
block.instructions[block.instructions.size() - 2].get();
switch (static_cast<spv::Op>(maybe_merge->opcode)) {
case spv::Op::OpLoopMerge:
block.successors.merge_block_id = GetOperand(maybe_merge, 0);
block.successors.continue_block_id = GetOperand(maybe_merge, 1);
break;
case spv::Op::OpSelectionMerge:
block.successors.merge_block_id = GetOperand(maybe_merge, 0);
break;
default:
break;
}
}
// Then look at the last instruction; it must be a branch
assert(block.instructions.size() >= 2);
const spv_parsed_instruction_t* branch = block.instructions.back().get();
switch (static_cast<spv::Op>(branch->opcode)) {
case spv::Op::OpBranch:
if (block.successors.merge_block_id != 0) {
block.successors.body_block_id = GetOperand(branch, 0);
} else {
block.successors.next_block_id = GetOperand(branch, 0);
}
break;
case spv::Op::OpBranchConditional:
block.successors.true_block_id = GetOperand(branch, 1);
block.successors.false_block_id = GetOperand(branch, 2);
break;
case spv::Op::OpSwitch:
for (uint32_t case_index = 1; case_index < branch->num_operands;
case_index += 2) {
block.successors.case_block_ids.push_back(
GetOperand(branch, case_index));
}
break;
default:
break;
}
}
return id_to_index;
}
// Helper to deal with nesting and non-existing ids / previously-assigned
// levels. It assigns a given nesting level `level` to the block identified by
// `id` (unless that block already has a nesting level assigned).
void Nest(ControlFlowGraph& cfg,
const std::unordered_map<uint32_t, uint32_t>& id_to_index,
uint32_t id, uint32_t level) {
if (id == 0) {
return;
}
const uint32_t block_index = id_to_index.at(id);
SingleBlock& block = cfg.blocks[block_index];
if (!block.nest_level_assigned) {
block.nest_level = level;
block.nest_level_assigned = true;
}
}
// For a given block, assign nesting level to its successors.
void NestSuccessors(ControlFlowGraph& cfg, const SingleBlock& block,
const std::unordered_map<uint32_t, uint32_t>& id_to_index) {
assert(block.nest_level_assigned);
// Nest loops as such:
//
// %loop = OpLabel
// OpLoopMerge %merge %cont ...
// OpBranch %body
// %body = OpLabel
// Op...
// %cont = OpLabel
// Op...
// %merge = OpLabel
// Op...
//
// Nest conditional branches as such:
//
// %header = OpLabel
// OpSelectionMerge %merge ...
// OpBranchConditional ... %true %false
// %true = OpLabel
// Op...
// %false = OpLabel
// Op...
// %merge = OpLabel
// Op...
//
// Nest switch/case as such:
//
// %header = OpLabel
// OpSelectionMerge %merge ...
// OpSwitch ... %default ... %case0 ... %case1 ...
// %default = OpLabel
// Op...
// %case0 = OpLabel
// Op...
// %case1 = OpLabel
// Op...
// ...
// %merge = OpLabel
// Op...
//
// The following can be observed:
//
// - In all cases, the merge block has the same nesting as this block
// - The continue block of loops is nested 1 level deeper
// - The body/branches/cases are nested 2 levels deeper
//
// Back branches to the header block, branches to the merge block, etc
// are correctly handled by processing the header block first (that is
// _this_ block, already processed), then following the above rules
// (in the same order) for any block that is not already processed.
Nest(cfg, id_to_index, block.successors.merge_block_id, block.nest_level);
Nest(cfg, id_to_index, block.successors.continue_block_id,
block.nest_level + 1);
Nest(cfg, id_to_index, block.successors.true_block_id, block.nest_level + 2);
Nest(cfg, id_to_index, block.successors.false_block_id, block.nest_level + 2);
Nest(cfg, id_to_index, block.successors.body_block_id, block.nest_level + 2);
Nest(cfg, id_to_index, block.successors.next_block_id, block.nest_level);
for (uint32_t case_block_id : block.successors.case_block_ids) {
Nest(cfg, id_to_index, case_block_id, block.nest_level + 2);
}
}
struct StackEntry {
// The index of the block (in ControlFlowGraph::blocks) to process.
uint32_t block_index;
// Whether this is the pre or post visit of the block. Because a post-visit
// traversal is needed, the same block is pushed back on the stack on
// pre-visit so it can be visited again on post-visit.
bool post_visit = false;
};
// Helper to deal with DFS traversal and non-existing ids
void VisitSuccesor(std::stack<StackEntry>* dfs_stack,
const std::unordered_map<uint32_t, uint32_t>& id_to_index,
uint32_t id) {
if (id != 0) {
dfs_stack->push({id_to_index.at(id), false});
}
}
// Given the control flow graph, calculates and returns the reverse post-order
// ordering of the blocks. The blocks are then disassembled in that order for
// readability.
std::vector<uint32_t> OrderBlocks(
ControlFlowGraph& cfg,
const std::unordered_map<uint32_t, uint32_t>& id_to_index) {
std::vector<uint32_t> post_order;
// Nest level of a function's first block is 0.
cfg.blocks[0].nest_level = 0;
cfg.blocks[0].nest_level_assigned = true;
// Stack of block indices as they are visited.
std::stack<StackEntry> dfs_stack;
dfs_stack.push({0, false});
std::set<uint32_t> visited;
while (!dfs_stack.empty()) {
const uint32_t block_index = dfs_stack.top().block_index;
const bool post_visit = dfs_stack.top().post_visit;
dfs_stack.pop();
// If this is the second time the block is visited, that's the post-order
// visit.
if (post_visit) {
post_order.push_back(block_index);
continue;
}
// If already visited, another path got to it first (like a case
// fallthrough), avoid reprocessing it.
if (visited.count(block_index) > 0) {
continue;
}
visited.insert(block_index);
// Push it back in the stack for post-order visit
dfs_stack.push({block_index, true});
SingleBlock& block = cfg.blocks[block_index];
// Assign nest levels of successors right away. The successors are either
// nested under this block, or are back or forward edges to blocks outside
// this nesting level (no farther than the merge block), whose nesting
// levels are already assigned before this block is visited.
NestSuccessors(cfg, block, id_to_index);
block.reachable = true;
// The post-order visit yields the order in which the blocks are naturally
// ordered _backwards_. So blocks to be ordered last should be visited
// first. In other words, they should be pushed to the DFS stack last.
VisitSuccesor(&dfs_stack, id_to_index, block.successors.true_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.false_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.body_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.next_block_id);
for (uint32_t case_block_id : block.successors.case_block_ids) {
VisitSuccesor(&dfs_stack, id_to_index, case_block_id);
}
VisitSuccesor(&dfs_stack, id_to_index, block.successors.continue_block_id);
VisitSuccesor(&dfs_stack, id_to_index, block.successors.merge_block_id);
}
std::vector<uint32_t> order(post_order.rbegin(), post_order.rend());
// Finally, dump all unreachable blocks at the end
for (size_t index = 0; index < cfg.blocks.size(); ++index) {
SingleBlock& block = cfg.blocks[index];
if (!block.reachable) {
order.push_back(static_cast<uint32_t>(index));
block.nest_level = 0;
block.nest_level_assigned = true;
}
}
return order;
}
void Disassembler::EmitCFG() {
// Build the CFG edges. At the same time, build an ID->block index map to
// simplify building the CFG edges.
const std::unordered_map<uint32_t, uint32_t> id_to_index =
BuildControlFlowGraph(current_function_cfg_);
// Walk the CFG in reverse post-order to find the best ordering of blocks for
// presentation
std::vector<uint32_t> block_order =
OrderBlocks(current_function_cfg_, id_to_index);
assert(block_order.size() == current_function_cfg_.blocks.size());
// Walk the CFG either in block order or input order based on whether the
// reorder_blocks_ option is given.
for (uint32_t index = 0; index < current_function_cfg_.blocks.size();
++index) {
const uint32_t block_index = reorder_blocks_ ? block_order[index] : index;
const SingleBlock& block = current_function_cfg_.blocks[block_index];
// Emit instructions for this block
size_t byte_offset = block.byte_offset;
assert(block.nest_level_assigned);
for (const ParsedInstruction& inst : block.instructions) {
instruction_disassembler_.EmitInstructionInBlock(*inst.get(), byte_offset,
block.nest_level);
byte_offset += inst.get()->num_words * sizeof(uint32_t);
}
}
current_function_cfg_.blocks.clear();
}
spv_result_t Disassembler::SaveTextResult(spv_text* text_result) const {
if (!print_) {
size_t length = text_.str().size();
char* str = new char[length + 1];
if (!str) return SPV_ERROR_OUT_OF_MEMORY;
strncpy(str, text_.str().c_str(), length + 1);
spv_text text = new spv_text_t();
if (!text) {
delete[] str;
return SPV_ERROR_OUT_OF_MEMORY;
}
text->str = str;
text->length = length;
*text_result = text;
}
return SPV_SUCCESS;
}
spv_result_t DisassembleHeader(void* user_data, spv_endianness_t endian,
uint32_t /* magic */, uint32_t version,
uint32_t generator, uint32_t id_bound,
uint32_t schema) {
assert(user_data);
auto disassembler = static_cast<Disassembler*>(user_data);
return disassembler->HandleHeader(endian, version, generator, id_bound,
schema);
}
spv_result_t DisassembleInstruction(
void* user_data, const spv_parsed_instruction_t* parsed_instruction) {
assert(user_data);
auto disassembler = static_cast<Disassembler*>(user_data);
return disassembler->HandleInstruction(*parsed_instruction);
}
// Simple wrapper class to provide extra data necessary for targeted
// instruction disassembly.
class WrappedDisassembler {
public:
WrappedDisassembler(Disassembler* dis, const uint32_t* binary, size_t wc)
: disassembler_(dis), inst_binary_(binary), word_count_(wc) {}
Disassembler* disassembler() { return disassembler_; }
const uint32_t* inst_binary() const { return inst_binary_; }
size_t word_count() const { return word_count_; }
private:
Disassembler* disassembler_;
const uint32_t* inst_binary_;
const size_t word_count_;
};
spv_result_t DisassembleTargetHeader(void* user_data, spv_endianness_t endian,
uint32_t /* magic */, uint32_t version,
uint32_t generator, uint32_t id_bound,
uint32_t schema) {
assert(user_data);
auto wrapped = static_cast<WrappedDisassembler*>(user_data);
return wrapped->disassembler()->HandleHeader(endian, version, generator,
id_bound, schema);
}
spv_result_t DisassembleTargetInstruction(
void* user_data, const spv_parsed_instruction_t* parsed_instruction) {
assert(user_data);
auto wrapped = static_cast<WrappedDisassembler*>(user_data);
// Check if this is the instruction we want to disassemble.
if (wrapped->word_count() == parsed_instruction->num_words &&
std::equal(wrapped->inst_binary(),
wrapped->inst_binary() + wrapped->word_count(),
parsed_instruction->words)) {
// Found the target instruction. Disassemble it and signal that we should
// stop searching so we don't output the same instruction again.
if (auto error =
wrapped->disassembler()->HandleInstruction(*parsed_instruction))
return error;
return SPV_REQUESTED_TERMINATION;
}
return SPV_SUCCESS;
}
uint32_t GetLineLengthWithoutColor(const std::string line) {
// Currently, every added color is in the form \x1b...m, so instead of doing a
// lot of string comparisons with spvtools::clr::* strings, we just ignore
// those ranges.
uint32_t length = 0;
for (size_t i = 0; i < line.size(); ++i) {
if (line[i] == '\x1b') {
do {
++i;
} while (i < line.size() && line[i] != 'm');
continue;
}
++length;
}
return length;
}
constexpr int kStandardIndent = 15;
constexpr int kBlockNestIndent = 2;
constexpr int kBlockBodyIndentOffset = 2;
constexpr uint32_t kCommentColumn = 50;
} // namespace
namespace disassemble {
InstructionDisassembler::InstructionDisassembler(const AssemblyGrammar& grammar,
std::ostream& stream,
uint32_t options,
NameMapper name_mapper)
: grammar_(grammar),
stream_(stream),
print_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_PRINT, options)),
color_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COLOR, options)),
indent_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_INDENT, options)
? kStandardIndent
: 0),
nested_indent_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_NESTED_INDENT, options)),
comment_(spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_COMMENT, options)),
show_byte_offset_(
spvIsInBitfield(SPV_BINARY_TO_TEXT_OPTION_SHOW_BYTE_OFFSET, options)),
name_mapper_(std::move(name_mapper)),
last_instruction_comment_alignment_(0) {}
void InstructionDisassembler::EmitHeaderSpirv() { stream_ << "; SPIR-V\n"; }
void InstructionDisassembler::EmitHeaderVersion(uint32_t version) {
stream_ << "; Version: " << SPV_SPIRV_VERSION_MAJOR_PART(version) << "."
<< SPV_SPIRV_VERSION_MINOR_PART(version) << "\n";
}
void InstructionDisassembler::EmitHeaderGenerator(uint32_t generator) {
const char* generator_tool =
spvGeneratorStr(SPV_GENERATOR_TOOL_PART(generator));
stream_ << "; Generator: " << generator_tool;
// For unknown tools, print the numeric tool value.
if (0 == strcmp("Unknown", generator_tool)) {
stream_ << "(" << SPV_GENERATOR_TOOL_PART(generator) << ")";
}
// Print the miscellaneous part of the generator word on the same
// line as the tool name.
stream_ << "; " << SPV_GENERATOR_MISC_PART(generator) << "\n";
}
void InstructionDisassembler::EmitHeaderIdBound(uint32_t id_bound) {
stream_ << "; Bound: " << id_bound << "\n";
}
void InstructionDisassembler::EmitHeaderSchema(uint32_t schema) {
stream_ << "; Schema: " << schema << "\n";
}
void InstructionDisassembler::EmitInstruction(
const spv_parsed_instruction_t& inst, size_t inst_byte_offset) {
EmitInstructionImpl(inst, inst_byte_offset, 0, false);
}
void InstructionDisassembler::EmitInstructionInBlock(
const spv_parsed_instruction_t& inst, size_t inst_byte_offset,
uint32_t block_indent) {
EmitInstructionImpl(inst, inst_byte_offset, block_indent, true);
}
void InstructionDisassembler::EmitInstructionImpl(
const spv_parsed_instruction_t& inst, size_t inst_byte_offset,
uint32_t block_indent, bool is_in_block) {
auto opcode = static_cast<spv::Op>(inst.opcode);
// To better align the comments (if any), write the instruction to a line
// first so its length can be readily available.
std::ostringstream line;
if (nested_indent_ && opcode == spv::Op::OpLabel) {
// Separate the blocks by an empty line to make them easier to separate
stream_ << std::endl;
}
if (inst.result_id) {
SetBlue();
const std::string id_name = name_mapper_(inst.result_id);
if (indent_)
line << std::setw(std::max(0, indent_ - 3 - int(id_name.size())));
line << "%" << id_name;
ResetColor();
line << " = ";
} else {
line << std::string(indent_, ' ');
}
if (nested_indent_ && is_in_block) {
// Output OpLabel at the specified nest level, and instructions inside
// blocks nested a little more.
uint32_t indent = block_indent;
bool body_indent = opcode != spv::Op::OpLabel;
line << std::string(
indent * kBlockNestIndent + (body_indent ? kBlockBodyIndentOffset : 0),
' ');
}
line << "Op" << spvOpcodeString(opcode);
for (uint16_t i = 0; i < inst.num_operands; i++) {
const spv_operand_type_t type = inst.operands[i].type;
assert(type != SPV_OPERAND_TYPE_NONE);
if (type == SPV_OPERAND_TYPE_RESULT_ID) continue;
line << " ";
EmitOperand(line, inst, i);
}
// For the sake of comment generation, store information from some
// instructions for the future.
if (comment_) {
GenerateCommentForDecoratedId(inst);
}
std::ostringstream comments;
const char* comment_separator = "";
if (show_byte_offset_) {
SetGrey(comments);
auto saved_flags = comments.flags();
auto saved_fill = comments.fill();
comments << comment_separator << "0x" << std::setw(8) << std::hex
<< std::setfill('0') << inst_byte_offset;
comments.flags(saved_flags);
comments.fill(saved_fill);
ResetColor(comments);
comment_separator = ", ";
}
if (comment_ && opcode == spv::Op::OpName) {
const spv_parsed_operand_t& operand = inst.operands[0];
const uint32_t word = inst.words[operand.offset];
comments << comment_separator << "id %" << word;
comment_separator = ", ";
}
if (comment_ && inst.result_id && id_comments_.count(inst.result_id) > 0) {
comments << comment_separator << id_comments_[inst.result_id].str();
comment_separator = ", ";
}
stream_ << line.str();
if (!comments.str().empty()) {
// Align the comments
const uint32_t line_length = GetLineLengthWithoutColor(line.str());
uint32_t align = std::max(
{line_length + 2, last_instruction_comment_alignment_, kCommentColumn});
// Round up the alignment to a multiple of 4 for more niceness.
align = (align + 3) & ~0x3u;
last_instruction_comment_alignment_ = align;
stream_ << std::string(align - line_length, ' ') << "; " << comments.str();
} else {
last_instruction_comment_alignment_ = 0;
}
stream_ << "\n";
}
void InstructionDisassembler::GenerateCommentForDecoratedId(
const spv_parsed_instruction_t& inst) {
assert(comment_);
auto opcode = static_cast<spv::Op>(inst.opcode);
std::ostringstream partial;
uint32_t id = 0;
const char* separator = "";
switch (opcode) {
case spv::Op::OpDecorate:
// Take everything after `OpDecorate %id` and associate it with id.
id = inst.words[inst.operands[0].offset];
for (uint16_t i = 1; i < inst.num_operands; i++) {
partial << separator;
separator = " ";
EmitOperand(partial, inst, i);
}
break;
default:
break;
}
if (id == 0) {
return;
}
// Add the new comment to the comments of this id
std::ostringstream& id_comment = id_comments_[id];
if (!id_comment.str().empty()) {
id_comment << ", ";
}
id_comment << partial.str();
}
void InstructionDisassembler::EmitSectionComment(
const spv_parsed_instruction_t& inst, bool& inserted_decoration_space,
bool& inserted_debug_space, bool& inserted_type_space) {
auto opcode = static_cast<spv::Op>(inst.opcode);
if (comment_ && opcode == spv::Op::OpFunction) {
stream_ << std::endl;
if (nested_indent_) {
// Double the empty lines between Function sections since nested_indent_
// also separates blocks by a blank.
stream_ << std::endl;
}
stream_ << std::string(indent_, ' ');
stream_ << "; Function " << name_mapper_(inst.result_id) << std::endl;
}
if (comment_ && !inserted_decoration_space && spvOpcodeIsDecoration(opcode)) {
inserted_decoration_space = true;
stream_ << std::endl;
stream_ << std::string(indent_, ' ');
stream_ << "; Annotations" << std::endl;
}
if (comment_ && !inserted_debug_space && spvOpcodeIsDebug(opcode)) {
inserted_debug_space = true;
stream_ << std::endl;
stream_ << std::string(indent_, ' ');
stream_ << "; Debug Information" << std::endl;
}
if (comment_ && !inserted_type_space && spvOpcodeGeneratesType(opcode)) {
inserted_type_space = true;
stream_ << std::endl;
stream_ << std::string(indent_, ' ');
stream_ << "; Types, variables and constants" << std::endl;
}
}
void InstructionDisassembler::EmitOperand(std::ostream& stream,
const spv_parsed_instruction_t& inst,
const uint16_t operand_index) const {
assert(operand_index < inst.num_operands);
const spv_parsed_operand_t& operand = inst.operands[operand_index];
const uint32_t word = inst.words[operand.offset];
switch (operand.type) {
case SPV_OPERAND_TYPE_RESULT_ID:
assert(false && "<result-id> is not supposed to be handled here");
SetBlue(stream);
stream << "%" << name_mapper_(word);
break;
case SPV_OPERAND_TYPE_ID:
case SPV_OPERAND_TYPE_TYPE_ID:
case SPV_OPERAND_TYPE_SCOPE_ID:
case SPV_OPERAND_TYPE_MEMORY_SEMANTICS_ID:
SetYellow(stream);
stream << "%" << name_mapper_(word);
break;
case SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER: {
spv_ext_inst_desc ext_inst;
SetRed(stream);
if (grammar_.lookupExtInst(inst.ext_inst_type, word, &ext_inst) ==
SPV_SUCCESS) {
stream << ext_inst->name;
} else {
if (!spvExtInstIsNonSemantic(inst.ext_inst_type)) {
assert(false && "should have caught this earlier");
} else {
// for non-semantic instruction sets we can just print the number
stream << word;
}
}
} break;
case SPV_OPERAND_TYPE_SPEC_CONSTANT_OP_NUMBER: {
spv_opcode_desc opcode_desc;
if (grammar_.lookupOpcode(spv::Op(word), &opcode_desc))
assert(false && "should have caught this earlier");
SetRed(stream);
stream << opcode_desc->name;
} break;
case SPV_OPERAND_TYPE_LITERAL_INTEGER:
case SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER:
case SPV_OPERAND_TYPE_LITERAL_FLOAT: {
SetRed(stream);
EmitNumericLiteral(&stream, inst, operand);
ResetColor(stream);
} break;
case SPV_OPERAND_TYPE_LITERAL_STRING: {
stream << "\"";
SetGreen(stream);
std::string str = spvDecodeLiteralStringOperand(inst, operand_index);
for (char const& c : str) {
if (c == '"' || c == '\\') stream << '\\';
stream << c;
}
ResetColor(stream);
stream << '"';
} break;
case SPV_OPERAND_TYPE_CAPABILITY:
case SPV_OPERAND_TYPE_SOURCE_LANGUAGE:
case SPV_OPERAND_TYPE_EXECUTION_MODEL:
case SPV_OPERAND_TYPE_ADDRESSING_MODEL:
case SPV_OPERAND_TYPE_MEMORY_MODEL:
case SPV_OPERAND_TYPE_EXECUTION_MODE:
case SPV_OPERAND_TYPE_STORAGE_CLASS:
case SPV_OPERAND_TYPE_DIMENSIONALITY:
case SPV_OPERAND_TYPE_SAMPLER_ADDRESSING_MODE:
case SPV_OPERAND_TYPE_SAMPLER_FILTER_MODE:
case SPV_OPERAND_TYPE_SAMPLER_IMAGE_FORMAT:
case SPV_OPERAND_TYPE_FP_ROUNDING_MODE:
case SPV_OPERAND_TYPE_LINKAGE_TYPE:
case SPV_OPERAND_TYPE_ACCESS_QUALIFIER:
case SPV_OPERAND_TYPE_FUNCTION_PARAMETER_ATTRIBUTE:
case SPV_OPERAND_TYPE_DECORATION:
case SPV_OPERAND_TYPE_BUILT_IN:
case SPV_OPERAND_TYPE_GROUP_OPERATION:
case SPV_OPERAND_TYPE_KERNEL_ENQ_FLAGS:
case SPV_OPERAND_TYPE_KERNEL_PROFILING_INFO:
case SPV_OPERAND_TYPE_RAY_FLAGS:
case SPV_OPERAND_TYPE_RAY_QUERY_INTERSECTION:
case SPV_OPERAND_TYPE_RAY_QUERY_COMMITTED_INTERSECTION_TYPE:
case SPV_OPERAND_TYPE_RAY_QUERY_CANDIDATE_INTERSECTION_TYPE:
case SPV_OPERAND_TYPE_DEBUG_BASE_TYPE_ATTRIBUTE_ENCODING:
case SPV_OPERAND_TYPE_DEBUG_COMPOSITE_TYPE:
case SPV_OPERAND_TYPE_DEBUG_TYPE_QUALIFIER:
case SPV_OPERAND_TYPE_DEBUG_OPERATION:
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_BASE_TYPE_ATTRIBUTE_ENCODING:
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_COMPOSITE_TYPE:
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_TYPE_QUALIFIER:
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_OPERATION:
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_IMPORTED_ENTITY:
case SPV_OPERAND_TYPE_FPDENORM_MODE:
case SPV_OPERAND_TYPE_FPOPERATION_MODE:
case SPV_OPERAND_TYPE_QUANTIZATION_MODES:
case SPV_OPERAND_TYPE_FPENCODING:
case SPV_OPERAND_TYPE_OVERFLOW_MODES: {
spv_operand_desc entry;
if (grammar_.lookupOperand(operand.type, word, &entry))
assert(false && "should have caught this earlier");
stream << entry->name;
} break;
case SPV_OPERAND_TYPE_FP_FAST_MATH_MODE:
case SPV_OPERAND_TYPE_FUNCTION_CONTROL:
case SPV_OPERAND_TYPE_LOOP_CONTROL:
case SPV_OPERAND_TYPE_IMAGE:
case SPV_OPERAND_TYPE_MEMORY_ACCESS:
case SPV_OPERAND_TYPE_SELECTION_CONTROL:
case SPV_OPERAND_TYPE_DEBUG_INFO_FLAGS:
case SPV_OPERAND_TYPE_CLDEBUG100_DEBUG_INFO_FLAGS:
case SPV_OPERAND_TYPE_RAW_ACCESS_CHAIN_OPERANDS:
EmitMaskOperand(stream, operand.type, word);
break;
default:
if (spvOperandIsConcreteMask(operand.type)) {
EmitMaskOperand(stream, operand.type, word);
} else if (spvOperandIsConcrete(operand.type)) {
spv_operand_desc entry;
if (grammar_.lookupOperand(operand.type, word, &entry))
assert(false && "should have caught this earlier");
stream << entry->name;
} else {
assert(false && "unhandled or invalid case");
}
break;
}
ResetColor(stream);
}
void InstructionDisassembler::EmitMaskOperand(std::ostream& stream,
const spv_operand_type_t type,
const uint32_t word) const {
// Scan the mask from least significant bit to most significant bit. For each
// set bit, emit the name of that bit. Separate multiple names with '|'.
uint32_t remaining_word = word;
uint32_t mask;
int num_emitted = 0;
for (mask = 1; remaining_word; mask <<= 1) {
if (remaining_word & mask) {
remaining_word ^= mask;
spv_operand_desc entry;
if (grammar_.lookupOperand(type, mask, &entry))
assert(false && "should have caught this earlier");
if (num_emitted) stream << "|";
stream << entry->name;
num_emitted++;
}
}
if (!num_emitted) {
// An operand value of 0 was provided, so represent it by the name
// of the 0 value. In many cases, that's "None".
spv_operand_desc entry;
if (SPV_SUCCESS == grammar_.lookupOperand(type, 0, &entry))
stream << entry->name;
}
}
void InstructionDisassembler::ResetColor(std::ostream& stream) const {
if (color_) stream << spvtools::clr::reset{print_};
}
void InstructionDisassembler::SetGrey(std::ostream& stream) const {
if (color_) stream << spvtools::clr::grey{print_};
}
void InstructionDisassembler::SetBlue(std::ostream& stream) const {
if (color_) stream << spvtools::clr::blue{print_};
}
void InstructionDisassembler::SetYellow(std::ostream& stream) const {
if (color_) stream << spvtools::clr::yellow{print_};
}
void InstructionDisassembler::SetRed(std::ostream& stream) const {
if (color_) stream << spvtools::clr::red{print_};
}
void InstructionDisassembler::SetGreen(std::ostream& stream) const {
if (color_) stream << spvtools::clr::green{print_};
}
void InstructionDisassembler::ResetColor() { ResetColor(stream_); }
void InstructionDisassembler::SetGrey() { SetGrey(stream_); }
void InstructionDisassembler::SetBlue() { SetBlue(stream_); }
void InstructionDisassembler::SetYellow() { SetYellow(stream_); }
void InstructionDisassembler::SetRed() { SetRed(stream_); }
void InstructionDisassembler::SetGreen() { SetGreen(stream_); }
} // namespace disassemble
std::string spvInstructionBinaryToText(const spv_target_env env,
const uint32_t* instCode,
const size_t instWordCount,
const uint32_t* code,
const size_t wordCount,
const uint32_t options) {
spv_context context = spvContextCreate(env);
const AssemblyGrammar grammar(context);
if (!grammar.isValid()) {
spvContextDestroy(context);
return "";
}
// Generate friendly names for Ids if requested.
std::unique_ptr<FriendlyNameMapper> friendly_mapper;
NameMapper name_mapper = GetTrivialNameMapper();
if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) {
friendly_mapper = MakeUnique<FriendlyNameMapper>(context, code, wordCount);
name_mapper = friendly_mapper->GetNameMapper();
}
// Now disassemble!
Disassembler disassembler(grammar, options, name_mapper);
WrappedDisassembler wrapped(&disassembler, instCode, instWordCount);
spvBinaryParse(context, &wrapped, code, wordCount, DisassembleTargetHeader,
DisassembleTargetInstruction, nullptr);
spv_text text = nullptr;
std::string output;
if (disassembler.SaveTextResult(&text) == SPV_SUCCESS) {
output.assign(text->str, text->str + text->length);
// Drop trailing newline characters.
while (!output.empty() && output.back() == '\n') output.pop_back();
}
spvTextDestroy(text);
spvContextDestroy(context);
return output;
}
} // namespace spvtools
spv_result_t spvBinaryToText(const spv_const_context context,
const uint32_t* code, const size_t wordCount,
const uint32_t options, spv_text* pText,
spv_diagnostic* pDiagnostic) {
spv_context_t hijack_context = *context;
if (pDiagnostic) {
*pDiagnostic = nullptr;
spvtools::UseDiagnosticAsMessageConsumer(&hijack_context, pDiagnostic);
}
const spvtools::AssemblyGrammar grammar(&hijack_context);
if (!grammar.isValid()) return SPV_ERROR_INVALID_TABLE;
// Generate friendly names for Ids if requested.
std::unique_ptr<spvtools::FriendlyNameMapper> friendly_mapper;
spvtools::NameMapper name_mapper = spvtools::GetTrivialNameMapper();
if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) {
friendly_mapper = spvtools::MakeUnique<spvtools::FriendlyNameMapper>(
&hijack_context, code, wordCount);
name_mapper = friendly_mapper->GetNameMapper();
}
// Now disassemble!
spvtools::Disassembler disassembler(grammar, options, name_mapper);
if (auto error =
spvBinaryParse(&hijack_context, &disassembler, code, wordCount,
spvtools::DisassembleHeader,
spvtools::DisassembleInstruction, pDiagnostic)) {
return error;
}
return disassembler.SaveTextResult(pText);
}