| /* Opcode printing code for the WebAssembly target |
| Copyright (C) 2017-2022 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 <stdint.h> |
| |
| #include <limits.h> |
| #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)); |
| } |