Commit 152e60e3 authored by Alexei Starovoitov's avatar Alexei Starovoitov

Merge branch 'bpftool: Add LLVM as default library for disassembling JIT-ed programs'

Quentin Monnet says:

====================

To disassemble instructions for JIT-ed programs, bpftool has relied on the
libbfd library. This has been problematic in the past: libbfd's interface
is not meant to be stable and has changed several times, hence the
detection of the two related features from the Makefile
(disassembler-four-args and disassembler-init-styled). When it comes to
shipping bpftool, this has also caused issues with several distribution
maintainers unwilling to support the feature (for example, Debian's page
for binutils-dev, libbfd's package, says: "Note that building Debian
packages which depend on the shared libbfd is Not Allowed.").

This patchset adds support for LLVM as the primary library for
disassembling instructions for JIT-ed programs.

We keep libbfd as a fallback. One reason for this is that currently it
works well, we have all we need in terms of features detection in the
Makefile, so it provides a fallback for disassembling JIT-ed programs if
libbfd is installed but LLVM is not. The other reason is that libbfd
supports nfp instruction for Netronome's SmartNICs and can be used to
disassemble offloaded programs, something that LLVM cannot do (Niklas
confirmed that the feature is still in use). However, if libbfd's interface
breaks again in the future, we might reconsider keeping support for it.

v4:
  - Rebase to address a conflict with commit 2c76238e ("bpftool: Add
    "bootstrap" feature to version output").

v3:
  - Extend commit description (patch 6) with notes on llvm-dev and LLVM's
    disassembler stability.

v2:
  - Pass callback when creating the LLVM disassembler, so that the
    branch targets are printed as addresses (instead of byte offsets).
  - Add last commit to "support" other arch with LLVM, although we don't
    know any supported triple yet.
  - Use $(LLVM_CONFIG) instead of llvm-config in Makefile.
  - Pass components to llvm-config --libs to limit the number of
    libraries to pass on the command line, in Makefile.
  - Rebase split of FEATURE_TESTS and FEATURE_DISPLAY in Makefile.
====================
Signed-off-by: default avatarAlexei Starovoitov <ast@kernel.org>
parents 8f4bc15b 08b8191b
......@@ -7,10 +7,10 @@
Print bpftool's version number (similar to **bpftool version**), the
number of the libbpf version in use, and optional features that were
included when bpftool was compiled. Optional features include linking
against libbfd to provide the disassembler for JIT-ted programs
(**bpftool prog dump jited**) and usage of BPF skeletons (some
features like **bpftool prog profile** or showing pids associated to
BPF objects may rely on it).
against LLVM or libbfd to provide the disassembler for JIT-ted
programs (**bpftool prog dump jited**) and usage of BPF skeletons
(some features like **bpftool prog profile** or showing pids
associated to BPF objects may rely on it).
-j, --json
Generate JSON output. For commands that cannot produce JSON, this
......
......@@ -93,11 +93,22 @@ INSTALL ?= install
RM ?= rm -f
FEATURE_USER = .bpftool
FEATURE_TESTS = libbfd libbfd-liberty libbfd-liberty-z \
disassembler-four-args disassembler-init-styled libcap \
clang-bpf-co-re
FEATURE_DISPLAY = libbfd libbfd-liberty libbfd-liberty-z \
libcap clang-bpf-co-re
FEATURE_TESTS := clang-bpf-co-re
FEATURE_TESTS += llvm
FEATURE_TESTS += libcap
FEATURE_TESTS += libbfd
FEATURE_TESTS += libbfd-liberty
FEATURE_TESTS += libbfd-liberty-z
FEATURE_TESTS += disassembler-four-args
FEATURE_TESTS += disassembler-init-styled
FEATURE_DISPLAY := clang-bpf-co-re
FEATURE_DISPLAY += llvm
FEATURE_DISPLAY += libcap
FEATURE_DISPLAY += libbfd
FEATURE_DISPLAY += libbfd-liberty
FEATURE_DISPLAY += libbfd-liberty-z
check_feat := 1
NON_CHECK_FEAT_TARGETS := clean uninstall doc doc-clean doc-install doc-uninstall
......@@ -115,13 +126,6 @@ include $(FEATURES_DUMP)
endif
endif
ifeq ($(feature-disassembler-four-args), 1)
CFLAGS += -DDISASM_FOUR_ARGS_SIGNATURE
endif
ifeq ($(feature-disassembler-init-styled), 1)
CFLAGS += -DDISASM_INIT_STYLED
endif
LIBS = $(LIBBPF) -lelf -lz
LIBS_BOOTSTRAP = $(LIBBPF_BOOTSTRAP) -lelf -lz
ifeq ($(feature-libcap), 1)
......@@ -133,21 +137,41 @@ include $(wildcard $(OUTPUT)*.d)
all: $(OUTPUT)bpftool
BFD_SRCS = jit_disasm.c
SRCS := $(wildcard *.c)
SRCS = $(filter-out $(BFD_SRCS),$(wildcard *.c))
ifeq ($(feature-libbfd),1)
LIBS += -lbfd -ldl -lopcodes
else ifeq ($(feature-libbfd-liberty),1)
LIBS += -lbfd -ldl -lopcodes -liberty
else ifeq ($(feature-libbfd-liberty-z),1)
LIBS += -lbfd -ldl -lopcodes -liberty -lz
ifeq ($(feature-llvm),1)
# If LLVM is available, use it for JIT disassembly
CFLAGS += -DHAVE_LLVM_SUPPORT
LLVM_CONFIG_LIB_COMPONENTS := mcdisassembler all-targets
CFLAGS += $(shell $(LLVM_CONFIG) --cflags --libs $(LLVM_CONFIG_LIB_COMPONENTS))
LIBS += $(shell $(LLVM_CONFIG) --libs $(LLVM_CONFIG_LIB_COMPONENTS))
LDFLAGS += $(shell $(LLVM_CONFIG) --ldflags)
else
# Fall back on libbfd
ifeq ($(feature-libbfd),1)
LIBS += -lbfd -ldl -lopcodes
else ifeq ($(feature-libbfd-liberty),1)
LIBS += -lbfd -ldl -lopcodes -liberty
else ifeq ($(feature-libbfd-liberty-z),1)
LIBS += -lbfd -ldl -lopcodes -liberty -lz
endif
# If one of the above feature combinations is set, we support libbfd
ifneq ($(filter -lbfd,$(LIBS)),)
CFLAGS += -DHAVE_LIBBFD_SUPPORT
# Libbfd interface changed over time, figure out what we need
ifeq ($(feature-disassembler-four-args), 1)
CFLAGS += -DDISASM_FOUR_ARGS_SIGNATURE
endif
ifeq ($(feature-disassembler-init-styled), 1)
CFLAGS += -DDISASM_INIT_STYLED
endif
endif
endif
ifneq ($(filter -lbfd,$(LIBS)),)
CFLAGS += -DHAVE_LIBBFD_SUPPORT
SRCS += $(BFD_SRCS)
ifeq ($(filter -DHAVE_LLVM_SUPPORT -DHAVE_LIBBFD_SUPPORT,$(CFLAGS)),)
# No support for JIT disassembly
SRCS := $(filter-out jit_disasm.c,$(SRCS))
endif
HOST_CFLAGS = $(subst -I$(LIBBPF_INCLUDE),-I$(LIBBPF_BOOTSTRAP_INCLUDE),\
......
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
......@@ -625,12 +627,11 @@ static int read_sysfs_netdev_hex_int(char *devname, const char *entry_name)
}
const char *
ifindex_to_bfd_params(__u32 ifindex, __u64 ns_dev, __u64 ns_ino,
const char **opt)
ifindex_to_arch(__u32 ifindex, __u64 ns_dev, __u64 ns_ino, const char **opt)
{
__maybe_unused int device_id;
char devname[IF_NAMESIZE];
int vendor_id;
int device_id;
if (!ifindex_to_name_ns(ifindex, ns_dev, ns_ino, devname)) {
p_err("Can't get net device name for ifindex %d: %s", ifindex,
......@@ -645,6 +646,7 @@ ifindex_to_bfd_params(__u32 ifindex, __u64 ns_dev, __u64 ns_ino,
}
switch (vendor_id) {
#ifdef HAVE_LIBBFD_SUPPORT
case 0x19ee:
device_id = read_sysfs_netdev_hex_int(devname, "device");
if (device_id != 0x4000 &&
......@@ -653,8 +655,10 @@ ifindex_to_bfd_params(__u32 ifindex, __u64 ns_dev, __u64 ns_ino,
p_info("Unknown NFP device ID, assuming it is NFP-6xxx arch");
*opt = "ctx4";
return "NFP-6xxx";
#endif /* HAVE_LIBBFD_SUPPORT */
/* No NFP support in LLVM, we have no valid triple to return. */
default:
p_err("Can't get bfd arch name for device vendor id 0x%04x",
p_err("Can't get arch name for device vendor id 0x%04x",
vendor_id);
return NULL;
}
......
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
// Copyright (C) 2020 Facebook
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <unistd.h>
#include <linux/err.h>
#include <bpf/libbpf.h>
......
......@@ -11,35 +11,151 @@
* Licensed under the GNU General Public License, version 2.0 (GPLv2)
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <bfd.h>
#include <dis-asm.h>
#include <sys/stat.h>
#include <limits.h>
#include <bpf/libbpf.h>
#ifdef HAVE_LLVM_SUPPORT
#include <llvm-c/Core.h>
#include <llvm-c/Disassembler.h>
#include <llvm-c/Target.h>
#include <llvm-c/TargetMachine.h>
#endif
#ifdef HAVE_LIBBFD_SUPPORT
#include <bfd.h>
#include <dis-asm.h>
#include <tools/dis-asm-compat.h>
#endif
#include "json_writer.h"
#include "main.h"
static void get_exec_path(char *tpath, size_t size)
static int oper_count;
#ifdef HAVE_LLVM_SUPPORT
#define DISASM_SPACER
typedef LLVMDisasmContextRef disasm_ctx_t;
static int printf_json(char *s)
{
s = strtok(s, " \t");
jsonw_string_field(json_wtr, "operation", s);
jsonw_name(json_wtr, "operands");
jsonw_start_array(json_wtr);
oper_count = 1;
while ((s = strtok(NULL, " \t,()")) != 0) {
jsonw_string(json_wtr, s);
oper_count++;
}
return 0;
}
/* This callback to set the ref_type is necessary to have the LLVM disassembler
* print PC-relative addresses instead of byte offsets for branch instruction
* targets.
*/
static const char *
symbol_lookup_callback(__maybe_unused void *disasm_info,
__maybe_unused uint64_t ref_value,
uint64_t *ref_type, __maybe_unused uint64_t ref_PC,
__maybe_unused const char **ref_name)
{
*ref_type = LLVMDisassembler_ReferenceType_InOut_None;
return NULL;
}
static int
init_context(disasm_ctx_t *ctx, const char *arch,
__maybe_unused const char *disassembler_options,
__maybe_unused unsigned char *image, __maybe_unused ssize_t len)
{
char *triple;
if (arch)
triple = LLVMNormalizeTargetTriple(arch);
else
triple = LLVMGetDefaultTargetTriple();
if (!triple) {
p_err("Failed to retrieve triple");
return -1;
}
*ctx = LLVMCreateDisasm(triple, NULL, 0, NULL, symbol_lookup_callback);
LLVMDisposeMessage(triple);
if (!*ctx) {
p_err("Failed to create disassembler");
return -1;
}
return 0;
}
static void destroy_context(disasm_ctx_t *ctx)
{
LLVMDisposeMessage(*ctx);
}
static int
disassemble_insn(disasm_ctx_t *ctx, unsigned char *image, ssize_t len, int pc)
{
char buf[256];
int count;
count = LLVMDisasmInstruction(*ctx, image + pc, len - pc, pc,
buf, sizeof(buf));
if (json_output)
printf_json(buf);
else
printf("%s", buf);
return count;
}
int disasm_init(void)
{
LLVMInitializeAllTargetInfos();
LLVMInitializeAllTargetMCs();
LLVMInitializeAllDisassemblers();
return 0;
}
#endif /* HAVE_LLVM_SUPPORT */
#ifdef HAVE_LIBBFD_SUPPORT
#define DISASM_SPACER "\t"
typedef struct {
struct disassemble_info *info;
disassembler_ftype disassemble;
bfd *bfdf;
} disasm_ctx_t;
static int get_exec_path(char *tpath, size_t size)
{
const char *path = "/proc/self/exe";
ssize_t len;
len = readlink(path, tpath, size - 1);
assert(len > 0);
if (len <= 0)
return -1;
tpath[len] = 0;
return 0;
}
static int oper_count;
static int printf_json(void *out, const char *fmt, va_list ap)
{
char *s;
......@@ -97,37 +213,44 @@ static int fprintf_json_styled(void *out,
return r;
}
void disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
const char *arch, const char *disassembler_options,
const struct btf *btf,
const struct bpf_prog_linfo *prog_linfo,
__u64 func_ksym, unsigned int func_idx,
bool linum)
static int init_context(disasm_ctx_t *ctx, const char *arch,
const char *disassembler_options,
unsigned char *image, ssize_t len)
{
const struct bpf_line_info *linfo = NULL;
disassembler_ftype disassemble;
struct disassemble_info info;
unsigned int nr_skip = 0;
int count, i, pc = 0;
struct disassemble_info *info;
char tpath[PATH_MAX];
bfd *bfdf;
if (!len)
return;
memset(tpath, 0, sizeof(tpath));
get_exec_path(tpath, sizeof(tpath));
if (get_exec_path(tpath, sizeof(tpath))) {
p_err("failed to create disasembler (get_exec_path)");
return -1;
}
bfdf = bfd_openr(tpath, NULL);
assert(bfdf);
assert(bfd_check_format(bfdf, bfd_object));
ctx->bfdf = bfd_openr(tpath, NULL);
if (!ctx->bfdf) {
p_err("failed to create disassembler (bfd_openr)");
return -1;
}
if (!bfd_check_format(ctx->bfdf, bfd_object)) {
p_err("failed to create disassembler (bfd_check_format)");
goto err_close;
}
bfdf = ctx->bfdf;
ctx->info = malloc(sizeof(struct disassemble_info));
if (!ctx->info) {
p_err("mem alloc failed");
goto err_close;
}
info = ctx->info;
if (json_output)
init_disassemble_info_compat(&info, stdout,
init_disassemble_info_compat(info, stdout,
(fprintf_ftype) fprintf_json,
fprintf_json_styled);
else
init_disassemble_info_compat(&info, stdout,
init_disassemble_info_compat(info, stdout,
(fprintf_ftype) fprintf,
fprintf_styled);
......@@ -139,28 +262,77 @@ void disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
bfdf->arch_info = inf;
} else {
p_err("No libbfd support for %s", arch);
return;
goto err_free;
}
}
info.arch = bfd_get_arch(bfdf);
info.mach = bfd_get_mach(bfdf);
info->arch = bfd_get_arch(bfdf);
info->mach = bfd_get_mach(bfdf);
if (disassembler_options)
info.disassembler_options = disassembler_options;
info.buffer = image;
info.buffer_length = len;
info->disassembler_options = disassembler_options;
info->buffer = image;
info->buffer_length = len;
disassemble_init_for_target(&info);
disassemble_init_for_target(info);
#ifdef DISASM_FOUR_ARGS_SIGNATURE
disassemble = disassembler(info.arch,
bfd_big_endian(bfdf),
info.mach,
bfdf);
ctx->disassemble = disassembler(info->arch,
bfd_big_endian(bfdf),
info->mach,
bfdf);
#else
disassemble = disassembler(bfdf);
ctx->disassemble = disassembler(bfdf);
#endif
assert(disassemble);
if (!ctx->disassemble) {
p_err("failed to create disassembler");
goto err_free;
}
return 0;
err_free:
free(info);
err_close:
bfd_close(ctx->bfdf);
return -1;
}
static void destroy_context(disasm_ctx_t *ctx)
{
free(ctx->info);
bfd_close(ctx->bfdf);
}
static int
disassemble_insn(disasm_ctx_t *ctx, __maybe_unused unsigned char *image,
__maybe_unused ssize_t len, int pc)
{
return ctx->disassemble(pc, ctx->info);
}
int disasm_init(void)
{
bfd_init();
return 0;
}
#endif /* HAVE_LIBBPFD_SUPPORT */
int disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
const char *arch, const char *disassembler_options,
const struct btf *btf,
const struct bpf_prog_linfo *prog_linfo,
__u64 func_ksym, unsigned int func_idx,
bool linum)
{
const struct bpf_line_info *linfo = NULL;
unsigned int nr_skip = 0;
int count, i, pc = 0;
disasm_ctx_t ctx;
if (!len)
return -1;
if (init_context(&ctx, arch, disassembler_options, image, len))
return -1;
if (json_output)
jsonw_start_array(json_wtr);
......@@ -185,10 +357,11 @@ void disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
if (linfo)
btf_dump_linfo_plain(btf, linfo, "; ",
linum);
printf("%4x:\t", pc);
printf("%4x:" DISASM_SPACER, pc);
}
count = disassemble(pc, &info);
count = disassemble_insn(&ctx, image, len, pc);
if (json_output) {
/* Operand array, was started in fprintf_json. Before
* that, make sure we have a _null_ value if no operand
......@@ -224,11 +397,7 @@ void disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
if (json_output)
jsonw_end_array(json_wtr);
bfd_close(bfdf);
}
destroy_context(&ctx);
int disasm_init(void)
{
bfd_init();
return 0;
}
......@@ -119,6 +119,11 @@ static int do_version(int argc, char **argv)
#else
const bool has_libbfd = false;
#endif
#ifdef HAVE_LLVM_SUPPORT
const bool has_llvm = true;
#else
const bool has_llvm = false;
#endif
#ifdef BPFTOOL_WITHOUT_SKELETONS
const bool has_skeletons = false;
#else
......@@ -154,6 +159,7 @@ static int do_version(int argc, char **argv)
jsonw_name(json_wtr, "features");
jsonw_start_object(json_wtr); /* features */
jsonw_bool_field(json_wtr, "libbfd", has_libbfd);
jsonw_bool_field(json_wtr, "llvm", has_llvm);
jsonw_bool_field(json_wtr, "libbpf_strict", !legacy_libbpf);
jsonw_bool_field(json_wtr, "skeletons", has_skeletons);
jsonw_bool_field(json_wtr, "bootstrap", bootstrap);
......@@ -172,6 +178,7 @@ static int do_version(int argc, char **argv)
printf("using libbpf %s\n", libbpf_version_string());
printf("features:");
print_feature("libbfd", has_libbfd, &nb_features);
print_feature("llvm", has_llvm, &nb_features);
print_feature("libbpf_strict", !legacy_libbpf, &nb_features);
print_feature("skeletons", has_skeletons, &nb_features);
print_feature("bootstrap", bootstrap, &nb_features);
......
......@@ -172,27 +172,28 @@ int map_parse_fds(int *argc, char ***argv, int **fds);
int map_parse_fd_and_info(int *argc, char ***argv, void *info, __u32 *info_len);
struct bpf_prog_linfo;
#ifdef HAVE_LIBBFD_SUPPORT
void disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
const char *arch, const char *disassembler_options,
const struct btf *btf,
const struct bpf_prog_linfo *prog_linfo,
__u64 func_ksym, unsigned int func_idx,
bool linum);
#if defined(HAVE_LLVM_SUPPORT) || defined(HAVE_LIBBFD_SUPPORT)
int disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
const char *arch, const char *disassembler_options,
const struct btf *btf,
const struct bpf_prog_linfo *prog_linfo,
__u64 func_ksym, unsigned int func_idx,
bool linum);
int disasm_init(void);
#else
static inline
void disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
const char *arch, const char *disassembler_options,
const struct btf *btf,
const struct bpf_prog_linfo *prog_linfo,
__u64 func_ksym, unsigned int func_idx,
bool linum)
int disasm_print_insn(unsigned char *image, ssize_t len, int opcodes,
const char *arch, const char *disassembler_options,
const struct btf *btf,
const struct bpf_prog_linfo *prog_linfo,
__u64 func_ksym, unsigned int func_idx,
bool linum)
{
return 0;
}
static inline int disasm_init(void)
{
p_err("No libbfd support");
p_err("No JIT disassembly support");
return -1;
}
#endif
......@@ -202,8 +203,7 @@ void print_hex_data_json(uint8_t *data, size_t len);
unsigned int get_page_size(void);
unsigned int get_possible_cpus(void);
const char *
ifindex_to_bfd_params(__u32 ifindex, __u64 ns_dev, __u64 ns_ino,
const char **opt);
ifindex_to_arch(__u32 ifindex, __u64 ns_dev, __u64 ns_ino, const char **opt);
struct btf_dumper {
const struct btf *btf;
......
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/err.h>
......
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
// Copyright (C) 2018 Facebook
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
......
......@@ -2,7 +2,9 @@
// Copyright (C) 2018 Facebook
// Author: Yonghong Song <yhs@fb.com>
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
......
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
......@@ -762,10 +764,8 @@ prog_dump(struct bpf_prog_info *info, enum dump_mode mode,
const char *name = NULL;
if (info->ifindex) {
name = ifindex_to_bfd_params(info->ifindex,
info->netns_dev,
info->netns_ino,
&disasm_opt);
name = ifindex_to_arch(info->ifindex, info->netns_dev,
info->netns_ino, &disasm_opt);
if (!name)
goto exit_free;
}
......@@ -820,10 +820,11 @@ prog_dump(struct bpf_prog_info *info, enum dump_mode mode,
printf("%s:\n", sym_name);
}
disasm_print_insn(img, lens[i], opcodes,
name, disasm_opt, btf,
prog_linfo, ksyms[i], i,
linum);
if (disasm_print_insn(img, lens[i], opcodes,
name, disasm_opt, btf,
prog_linfo, ksyms[i], i,
linum))
goto exit_free;
img += lens[i];
......@@ -836,8 +837,10 @@ prog_dump(struct bpf_prog_info *info, enum dump_mode mode,
if (json_output)
jsonw_end_array(json_wtr);
} else {
disasm_print_insn(buf, member_len, opcodes, name,
disasm_opt, btf, NULL, 0, 0, false);
if (disasm_print_insn(buf, member_len, opcodes, name,
disasm_opt, btf, NULL, 0, 0,
false))
goto exit_free;
}
} else if (visual) {
if (json_output)
......
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
/* Copyright (C) 2018 Netronome Systems, Inc. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment