// 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 #include #include #include #include #include #include #include #include #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 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(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 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 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 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(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 BuildControlFlowGraph( ControlFlowGraph& cfg) { std::unordered_map 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(block.instructions[0].get()->opcode) == spv::Op::OpLabel); const uint32_t id = block.instructions[0].get()->result_id; id_to_index[id] = static_cast(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(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(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& 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& 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* dfs_stack, const std::unordered_map& 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 OrderBlocks( ControlFlowGraph& cfg, const std::unordered_map& id_to_index) { std::vector 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 dfs_stack; dfs_stack.push({0, false}); std::set 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 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(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 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 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(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(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(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(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(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(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(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 && " 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 friendly_mapper; NameMapper name_mapper = GetTrivialNameMapper(); if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) { friendly_mapper = MakeUnique(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 friendly_mapper; spvtools::NameMapper name_mapper = spvtools::GetTrivialNameMapper(); if (options & SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES) { friendly_mapper = spvtools::MakeUnique( &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); }