/* Opcode printing code for the WebAssembly target Copyright (C) 2017-2023 Free Software Foundation, Inc. This file is part of libopcodes. This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */ #include "sysdep.h" #include "disassemble.h" #include "opintl.h" #include "safe-ctype.h" #include "floatformat.h" #include "libiberty.h" #include "elf-bfd.h" #include "elf/internal.h" #include "elf/wasm32.h" #include #include #ifndef CHAR_BIT #define CHAR_BIT 8 #endif /* Type names for blocks and signatures. */ #define BLOCK_TYPE_NONE 0x40 #define BLOCK_TYPE_I32 0x7f #define BLOCK_TYPE_I64 0x7e #define BLOCK_TYPE_F32 0x7d #define BLOCK_TYPE_F64 0x7c enum wasm_class { wasm_typed, wasm_special, wasm_break, wasm_break_if, wasm_break_table, wasm_return, wasm_call, wasm_call_import, wasm_call_indirect, wasm_get_local, wasm_set_local, wasm_tee_local, wasm_drop, wasm_constant_i32, wasm_constant_i64, wasm_constant_f32, wasm_constant_f64, wasm_unary, wasm_binary, wasm_conv, wasm_load, wasm_store, wasm_select, wasm_relational, wasm_eqz, wasm_current_memory, wasm_grow_memory, wasm_signature }; struct wasm32_private_data { bool print_registers; bool print_well_known_globals; /* Limit valid symbols to those with a given prefix. */ const char *section_prefix; }; typedef struct { const char *name; const char *description; } wasm32_options_t; static const wasm32_options_t options[] = { { "registers", N_("Disassemble \"register\" names") }, { "globals", N_("Name well-known globals") }, }; #define WASM_OPCODE(opcode, name, intype, outtype, clas, signedness) \ { name, wasm_ ## clas, opcode }, struct wasm32_opcode_s { const char *name; enum wasm_class clas; unsigned char opcode; } wasm32_opcodes[] = { #include "opcode/wasm.h" { NULL, 0, 0 } }; /* Parse the disassembler options in OPTS and initialize INFO. */ static void parse_wasm32_disassembler_options (struct disassemble_info *info, const char *opts) { struct wasm32_private_data *private = info->private_data; while (opts != NULL) { if (startswith (opts, "registers")) private->print_registers = true; else if (startswith (opts, "globals")) private->print_well_known_globals = true; opts = strchr (opts, ','); if (opts) opts++; } } /* Check whether SYM is valid. Special-case absolute symbols, which are unhelpful to print, and arguments to a "call" insn, which we want to be in a section matching a given prefix. */ static bool wasm32_symbol_is_valid (asymbol *sym, struct disassemble_info *info) { struct wasm32_private_data *private_data = info->private_data; if (sym == NULL) return false; if (strcmp(sym->section->name, "*ABS*") == 0) return false; if (private_data && private_data->section_prefix != NULL && strncmp (sym->section->name, private_data->section_prefix, strlen (private_data->section_prefix))) return false; return true; } /* Initialize the disassembler structures for INFO. */ void disassemble_init_wasm32 (struct disassemble_info *info) { if (info->private_data == NULL) { static struct wasm32_private_data private; private.print_registers = false; private.print_well_known_globals = false; private.section_prefix = NULL; info->private_data = &private; } if (info->disassembler_options) { parse_wasm32_disassembler_options (info, info->disassembler_options); info->disassembler_options = NULL; } info->symbol_is_valid = wasm32_symbol_is_valid; } /* Read an LEB128-encoded integer from INFO at address PC, reading one byte at a time. Set ERROR_RETURN if no complete integer could be read, LENGTH_RETURN to the number oof bytes read (including bytes in incomplete numbers). SIGN means interpret the number as SLEB128. Unfortunately, this is a duplicate of wasm-module.c's wasm_read_leb128 (). */ static uint64_t wasm_read_leb128 (bfd_vma pc, struct disassemble_info *info, bool *error_return, unsigned int *length_return, bool sign) { uint64_t result = 0; unsigned int num_read = 0; unsigned int shift = 0; unsigned char byte = 0; unsigned char lost, mask; int status = 1; while (info->read_memory_func (pc + num_read, &byte, 1, info) == 0) { num_read++; if (shift < CHAR_BIT * sizeof (result)) { result |= ((uint64_t) (byte & 0x7f)) << shift; /* These bits overflowed. */ lost = byte ^ (result >> shift); /* And this is the mask of possible overflow bits. */ mask = 0x7f ^ ((uint64_t) 0x7f << shift >> shift); shift += 7; } else { lost = byte; mask = 0x7f; } if ((lost & mask) != (sign && (int64_t) result < 0 ? mask : 0)) status |= 2; if ((byte & 0x80) == 0) { status &= ~1; if (sign && shift < CHAR_BIT * sizeof (result) && (byte & 0x40)) result |= -((uint64_t) 1 << shift); break; } } if (length_return != NULL) *length_return = num_read; if (error_return != NULL) *error_return = status != 0; return result; } /* Read a 32-bit IEEE float from PC using INFO, convert it to a host double, and store it at VALUE. */ static int read_f32 (double *value, bfd_vma pc, struct disassemble_info *info) { bfd_byte buf[4]; if (info->read_memory_func (pc, buf, sizeof (buf), info)) return -1; floatformat_to_double (&floatformat_ieee_single_little, buf, value); return sizeof (buf); } /* Read a 64-bit IEEE float from PC using INFO, convert it to a host double, and store it at VALUE. */ static int read_f64 (double *value, bfd_vma pc, struct disassemble_info *info) { bfd_byte buf[8]; if (info->read_memory_func (pc, buf, sizeof (buf), info)) return -1; floatformat_to_double (&floatformat_ieee_double_little, buf, value); return sizeof (buf); } /* Main disassembly routine. Disassemble insn at PC using INFO. */ int print_insn_wasm32 (bfd_vma pc, struct disassemble_info *info) { unsigned char opcode; struct wasm32_opcode_s *op; bfd_byte buffer[16]; void *stream = info->stream; fprintf_ftype prin = info->fprintf_func; struct wasm32_private_data *private_data = info->private_data; uint64_t val; int len; unsigned int bytes_read; bool error; if (info->read_memory_func (pc, buffer, 1, info)) return -1; opcode = buffer[0]; for (op = wasm32_opcodes; op->name; op++) if (op->opcode == opcode) break; if (!op->name) { prin (stream, "\t.byte 0x%02x\n", buffer[0]); return 1; } len = 1; prin (stream, "\t"); prin (stream, "%s", op->name); if (op->clas == wasm_typed) { val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); if (error) return -1; len += bytes_read; switch (val) { case BLOCK_TYPE_NONE: prin (stream, "[]"); break; case BLOCK_TYPE_I32: prin (stream, "[i]"); break; case BLOCK_TYPE_I64: prin (stream, "[l]"); break; case BLOCK_TYPE_F32: prin (stream, "[f]"); break; case BLOCK_TYPE_F64: prin (stream, "[d]"); break; default: return -1; } } switch (op->clas) { case wasm_special: case wasm_eqz: case wasm_binary: case wasm_unary: case wasm_conv: case wasm_relational: case wasm_drop: case wasm_signature: case wasm_call_import: case wasm_typed: case wasm_select: break; case wasm_break_table: { uint32_t target_count, i; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); target_count = val; if (error || target_count != val || target_count == (uint32_t) -1) return -1; len += bytes_read; prin (stream, " %u", target_count); for (i = 0; i < target_count + 1; i++) { uint32_t target; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); target = val; if (error || target != val) return -1; len += bytes_read; prin (stream, " %u", target); } } break; case wasm_break: case wasm_break_if: { uint32_t depth; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); depth = val; if (error || depth != val) return -1; len += bytes_read; prin (stream, " %u", depth); } break; case wasm_return: break; case wasm_constant_i32: case wasm_constant_i64: val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, true); if (error) return -1; len += bytes_read; prin (stream, " %" PRId64, val); break; case wasm_constant_f32: { double fconstant; int ret; /* This appears to be the best we can do, even though we're using host doubles for WebAssembly floats. */ ret = read_f32 (&fconstant, pc + len, info); if (ret < 0) return -1; len += ret; prin (stream, " %.9g", fconstant); } break; case wasm_constant_f64: { double fconstant; int ret; ret = read_f64 (&fconstant, pc + len, info); if (ret < 0) return -1; len += ret; prin (stream, " %.17g", fconstant); } break; case wasm_call: { uint32_t function_index; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); function_index = val; if (error || function_index != val) return -1; len += bytes_read; prin (stream, " "); private_data->section_prefix = ".space.function_index"; (*info->print_address_func) ((bfd_vma) function_index, info); private_data->section_prefix = NULL; } break; case wasm_call_indirect: { uint32_t type_index, xtra_index; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); type_index = val; if (error || type_index != val) return -1; len += bytes_read; prin (stream, " %u", type_index); val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); xtra_index = val; if (error || xtra_index != val) return -1; len += bytes_read; prin (stream, " %u", xtra_index); } break; case wasm_get_local: case wasm_set_local: case wasm_tee_local: { uint32_t local_index; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); local_index = val; if (error || local_index != val) return -1; len += bytes_read; prin (stream, " %u", local_index); if (strcmp (op->name + 4, "local") == 0) { static const char *locals[] = { "$dpc", "$sp1", "$r0", "$r1", "$rpc", "$pc0", "$rp", "$fp", "$sp", "$r2", "$r3", "$r4", "$r5", "$r6", "$r7", "$i0", "$i1", "$i2", "$i3", "$i4", "$i5", "$i6", "$i7", "$f0", "$f1", "$f2", "$f3", "$f4", "$f5", "$f6", "$f7", }; if (private_data->print_registers && local_index < ARRAY_SIZE (locals)) prin (stream, " <%s>", locals[local_index]); } else { static const char *globals[] = { "$got", "$plt", "$gpo" }; if (private_data->print_well_known_globals && local_index < ARRAY_SIZE (globals)) prin (stream, " <%s>", globals[local_index]); } } break; case wasm_grow_memory: case wasm_current_memory: { uint32_t reserved_size; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); reserved_size = val; if (error || reserved_size != val) return -1; len += bytes_read; prin (stream, " %u", reserved_size); } break; case wasm_load: case wasm_store: { uint32_t flags, offset; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); flags = val; if (error || flags != val) return -1; len += bytes_read; val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false); offset = val; if (error || offset != val) return -1; len += bytes_read; prin (stream, " a=%u %u", flags, offset); } break; } return len; } /* Print valid disassembler options to STREAM. */ void print_wasm32_disassembler_options (FILE *stream) { unsigned int i, max_len = 0; fprintf (stream, _("\ The following WebAssembly-specific disassembler options are supported for use\n\ with the -M switch:\n")); for (i = 0; i < ARRAY_SIZE (options); i++) { unsigned int len = strlen (options[i].name); if (max_len < len) max_len = len; } for (i = 0, max_len++; i < ARRAY_SIZE (options); i++) fprintf (stream, " %s%*c %s\n", options[i].name, (int)(max_len - strlen (options[i].name)), ' ', _(options[i].description)); }