This is the mail archive of the
gdb-patches@sourceware.org
mailing list for the GDB project.
Re: [RFA] Add Windows x64 SEH unwinder
- From: Mark Kettenis <mark dot kettenis at xs4all dot nl>
- To: gingold at adacore dot com
- Cc: gdb-patches at sourceware dot org, ktietz70 at googlemail dot com
- Date: Sat, 16 Jun 2012 12:39:41 +0200 (CEST)
- Subject: Re: [RFA] Add Windows x64 SEH unwinder
- References: <F9D092E9-5627-4EEA-B0C8-0B04BD6A4666@adacore.com>
> From: Tristan Gingold <gingold@adacore.com>
> Date: Fri, 15 Jun 2012 10:09:56 +0200
>
> Hi,
>
> the Windows x64 ABI specifies unwind data info in order to unwind
> frames for propagating exceptions or getting back traces. GCC emits
> this infos since version 4.7
>
> This patch adds an unwinder that reads these data. The main
> advantage is that gdb is now able to unwind through code compiled
> with other compilers (and through system libraries).
>
> I haven't run the gdb testsuite on Windows x64 (I don't know if this
> is doable), but I have manually tested it on a few executables,
> including gdb itself.
>
> Comments are welcome.
The amd64 codebase consistently uses the amd64_-prefix for structures,
variables and functions. If this is effectively *the* unwinder for
64-bit windows, I'd recommend "amd64_windows_". If you expect more
Windows-specfic unwinders (or think amd64_windows_ is too long)
"amd64_seh_" might be a good alternative.
> diff --git a/gdb/amd64-windows-tdep.c b/gdb/amd64-windows-tdep.c
> index 4a40f47..7070e06 100644
> --- a/gdb/amd64-windows-tdep.c
> +++ b/gdb/amd64-windows-tdep.c
> @@ -23,6 +23,12 @@
> #include "gdbtypes.h"
> #include "gdbcore.h"
> #include "regcache.h"
> +#include "objfiles.h"
> +#include "frame-unwind.h"
> +#include "coff/internal.h"
> +#include "coff/i386.h"
> +#include "coff/pe.h"
> +#include "libcoff.h"
>
> /* The registers used to pass integer arguments during a function call. */
> static int amd64_windows_dummy_call_integer_regs[] =
> @@ -153,6 +159,588 @@ amd64_skip_main_prologue (struct gdbarch *gdbarch, CORE_ADDR pc)
> return pc;
> }
>
> +struct x64_frame_cache
> +{
> + CORE_ADDR image_base; /* ImageBase for the module. */
> + CORE_ADDR start_address; /* Function start rva. */
> + CORE_ADDR pc; /* Next instruction to be executed. */
> + CORE_ADDR sp; /* Current sp. */
> +
> + CORE_ADDR prev_reg_addr[16]; /* Address of saved registers. */
> + CORE_ADDR prev_xmm_addr[16]; /* Likewise for xmm registers. */
> + /* These two next fields are set only for machine info frames. */
> + CORE_ADDR prev_rip_addr; /* Likewise for RIP. */
> + CORE_ADDR prev_rsp_addr; /* Likewise for RSP. */
> + CORE_ADDR prev_sp; /* Address of the previous frame. */
> +};
> +
> +/* Convert a Windows register number to gdb. */
> +static const enum amd64_regnum x64_w2gdb_regnum[] =
> +{
> + AMD64_RAX_REGNUM,
> + AMD64_RCX_REGNUM,
> + AMD64_RDX_REGNUM,
> + AMD64_RBX_REGNUM,
> + AMD64_RSP_REGNUM,
> + AMD64_RBP_REGNUM,
> + AMD64_RSI_REGNUM,
> + AMD64_RDI_REGNUM,
> + AMD64_R8_REGNUM,
> + AMD64_R9_REGNUM,
> + AMD64_R10_REGNUM,
> + AMD64_R11_REGNUM,
> + AMD64_R12_REGNUM,
> + AMD64_R13_REGNUM,
> + AMD64_R14_REGNUM,
> + AMD64_R15_REGNUM
> +};
> +
> +/* Try to recognize and decode an epilogue sequence. */
> +
> +static int
> +x64_frame_decode_epilogue (struct frame_info *this_frame,
> + struct x64_frame_cache *cache)
> +{
> + /* Not in a prologue, so maybe in an epilogue. For the rules, cf
> + http://msdn.microsoft.com/en-us/library/tawsa7cb.aspx
> + Furthermore, according to RtlVirtualUnwind, the complete list of
> + epilog marker is:
> + - ret [c3]
> + - ret n [c2 imm16]
> + - rep ret [f3 c3]
> + - jmp imm8 | imm32 [eb rel8] or [e9 rel32]
> + - jmp qword ptr imm32 - not handled
> + - rex jmp reg [4X ff eY]
> + I would add:
> + - pop reg [41 58-5f] or [58-5f]
> +
> + We don't care about the instruction that deallocate the frame:
> + if it hasn't been executed, we can safely decode the insns,
> + if it has been executed, the following epilog decoding will
> + work.
> + */
> + CORE_ADDR pc = cache->pc;
> + CORE_ADDR cur_sp = cache->sp;
> + struct gdbarch *gdbarch = get_frame_arch (this_frame);
> + enum bfd_endian byte_order = gdbarch_byte_order (gdbarch);
> +
> + while (1)
> + {
> + gdb_byte op;
> + gdb_byte rex;
> +
> + if (target_read_memory (pc, &op, 1) != 0)
> + return -1;
> +
> + if (op == 0xc3)
> + {
> + /* Ret. */
> + cache->prev_rip_addr = cur_sp;
> + cache->prev_sp = cur_sp + 8;
> + return 1;
> + }
> + else if (op == 0xeb)
> + {
> + /* jmp rel8 */
> + gdb_byte rel8;
> +
> + if (target_read_memory (pc + 1, &rel8, 1) != 0)
> + return -1;
> + pc = pc + 2 + (signed char)rel8;
> + }
> + else if (op == 0xec)
> + {
> + /* jmp rel32 */
> + gdb_byte rel32[4];
> +
> + if (target_read_memory (pc + 1, rel32, 4) != 0)
> + return -1;
> + pc = pc + 5 + extract_signed_integer (rel32, 4, byte_order);
> + }
> + else if (op >= 0x58 && op <= 0x5f)
> + {
> + /* pop reg */
> + cache->prev_reg_addr[x64_w2gdb_regnum[op & 0x0f]] = cur_sp;
> + cur_sp += 8;
> + }
> + else if (op == 0xc2)
> + {
> + /* ret n */
> + gdb_byte imm16[2];
> +
> + if (target_read_memory (pc + 1, imm16, 2) != 0)
> + return -1;
> + cache->prev_rip_addr = cur_sp;
> + cache->prev_sp = cur_sp
> + + extract_unsigned_integer (imm16, 4, byte_order);
> + return 1;
> + }
> + else if (op == 0xf3)
> + {
> + /* rep; ret */
> + gdb_byte op1;
> +
> + if (target_read_memory (pc + 2, &op1, 1) != 0)
> + return -1;
> + if (op1 != 0xc3)
> + return 0;
> +
> + cache->prev_rip_addr = cur_sp;
> + cache->prev_sp = cur_sp + 8;
> + return 1;
> + }
> + else if (op < 0x40 || op > 0x4f)
> + {
> + /* Not REX, so unknown. */
> + return 0;
> + }
> +
> + /* Got a REX prefix, read next byte. */
> + rex = op;
> + if (target_read_memory (pc + 1, &op, 1) != 0)
> + return -1;
> +
> + if (op >= 0x58 && op <= 0x5f)
> + {
> + /* pop reg */
> + unsigned int reg;
> +
> + reg = (op & 0x0f) | ((rex & 1) << 3);
> + cache->prev_reg_addr[x64_w2gdb_regnum[reg]] = cur_sp;
> + cur_sp += 8;
> + }
> + else if (op == 0xff)
> + {
> + /* rex jmp reg */
> + gdb_byte op1;
> + unsigned int reg;
> + gdb_byte buf[8];
> +
> + if (target_read_memory (pc + 2, &op1, 1) != 0)
> + return -1;
> + if ((op1 & 0xf8) != 0xe0)
> + return 0;
> + reg = (op1 & 0x0f) | ((rex & 1) << 3);
> +
> + get_frame_register (this_frame, x64_w2gdb_regnum[reg], buf);
> + pc = extract_unsigned_integer (buf, 8, byte_order);
> + }
> + else
> + return 0;
> +
> + /* Allow the user to break this loop. */
> + if (quit_flag)
> + return 0;
> + }
> +}
> +
> +/* Decode and execute unwind insns at UNWIND_INFO. */
> +
> +static void
> +x64_frame_decode_insns (struct frame_info *this_frame,
> + struct x64_frame_cache *cache,
> + CORE_ADDR unwind_info)
> +{
> + CORE_ADDR save_addr = 0;
> + CORE_ADDR cur_sp = cache->sp;
> + struct gdbarch *gdbarch = get_frame_arch (this_frame);
> + enum bfd_endian byte_order = gdbarch_byte_order (gdbarch);
> + int j;
> +
> + for (j = 0; ; j++)
> + {
> + struct external_pex64_unwind_info ex_ui;
> + gdb_byte insns[2 * 256];
> + gdb_byte *p;
> + gdb_byte *end_insns;
> + unsigned char codes_count;
> + unsigned char frame_reg;
> + unsigned char frame_off;
> +
> + /* Read and decode header. */
> + if (target_read_memory (cache->image_base + unwind_info,
> + (char *) &ex_ui, sizeof (ex_ui)) != 0)
> + return;
> +
> + if (unwind_debug)
> + fprintf_unfiltered
> + (gdb_stdlog,
> + "x64_frame_play_insn: "
> + "%s: ver: %02x, plgsz: %02x, cnt: %02x, frame: %02x\n",
> + paddress (gdbarch, unwind_info),
> + ex_ui.Version_Flags, ex_ui.SizeOfPrologue,
> + ex_ui.CountOfCodes, ex_ui.FrameRegisterOffset);
> +
> + /* Check version. */
> + if (PEX64_UWI_VERSION (ex_ui.Version_Flags) != 1)
> + return;
> +
> + if (j == 0
> + && (cache->pc >=
> + cache->image_base + cache->start_address + ex_ui.SizeOfPrologue))
> + {
> + /* Not in the prologue; try to decode an epilog. */
> + if (x64_frame_decode_epilogue (this_frame, cache) == 1)
> + return;
> +
> + /* Not in an epilog. Clear possible side effects. */
> + memset (cache->prev_reg_addr, 0, sizeof (cache->prev_reg_addr));
> + }
> +
> + codes_count = ex_ui.CountOfCodes;
> + frame_reg = PEX64_UWI_FRAMEREG (ex_ui.FrameRegisterOffset);
> +
> + if (frame_reg != 0)
> + {
> + /* According to msdn:
> + If an FP reg is used, then any unwind code taking an offset must
> + only be used the the FP reg is established in the prolog. */
> + gdb_byte buf[8];
> + int frreg = x64_w2gdb_regnum[frame_reg];
> +
> + get_frame_register (this_frame, frreg, buf);
> + save_addr = extract_unsigned_integer (buf, 8, byte_order);
> +
> + if (unwind_debug)
> + fprintf_unfiltered (gdb_stdlog, " frame_reg=%s, val=%s\n",
> + gdbarch_register_name (gdbarch, frreg),
> + paddress (gdbarch, save_addr));
> + }
> +
> + /* Read opcodes. */
> + if (codes_count != 0
> + && target_read_memory (cache->image_base + unwind_info
> + + sizeof (ex_ui),
> + insns, codes_count * 2) != 0)
> + return;
> +
> + end_insns = &insns[codes_count * 2];
> + for (p = insns; p < end_insns; p += 2)
> + {
> + int reg;
> +
> + if (unwind_debug)
> + fprintf_unfiltered
> + (gdb_stdlog, " op #%u: off=0x%02x, insn=0x%02x\n",
> + (unsigned) (p - insns), p[0], p[1]);
> +
> + /* Virtually execute the operation. */
> + if (cache->pc >= cache->image_base + cache->start_address + p[0])
> + {
> + /* If there is no frame registers defined, the current value of
> + rsp is used instead. */
> + if (frame_reg == 0)
> + save_addr = cur_sp;
> +
> + switch (PEX64_UNWCODE_CODE (p[1]))
> + {
> + case UWOP_PUSH_NONVOL:
> + /* Push pre-decrements RSP. */
> + reg = x64_w2gdb_regnum[PEX64_UNWCODE_INFO (p[1])];
> + cache->prev_reg_addr[reg] = cur_sp;
> + cur_sp += 8;
> + break;
> + case UWOP_ALLOC_LARGE:
> + if (PEX64_UNWCODE_INFO (p[1]) == 0)
> + cur_sp +=
> + 8 * extract_unsigned_integer (p + 2, 2, byte_order);
> + else if (PEX64_UNWCODE_INFO (p[1]) == 1)
> + cur_sp += extract_unsigned_integer (p + 2, 4, byte_order);
> + else
> + return;
> + break;
> + case UWOP_ALLOC_SMALL:
> + cur_sp += 8 + 8 * PEX64_UNWCODE_INFO (p[1]);
> + break;
> + case UWOP_SET_FPREG:
> + cur_sp = save_addr
> + - PEX64_UWI_FRAMEOFF (ex_ui.FrameRegisterOffset) * 16;
> + break;
> + case UWOP_SAVE_NONVOL:
> + reg = x64_w2gdb_regnum[PEX64_UNWCODE_INFO (p[1])];
> + cache->prev_reg_addr[reg] = save_addr
> + + 8 * extract_unsigned_integer (p + 2, 2, byte_order);
> + break;
> + case UWOP_SAVE_NONVOL_FAR:
> + reg = x64_w2gdb_regnum[PEX64_UNWCODE_INFO (p[1])];
> + cache->prev_reg_addr[reg] = save_addr
> + + 8 * extract_unsigned_integer (p + 2, 4, byte_order);
> + break;
> + case UWOP_SAVE_XMM128:
> + cache->prev_xmm_addr[PEX64_UNWCODE_INFO (p[1])] =
> + save_addr
> + + 8 * extract_unsigned_integer (p + 2, 2, byte_order);
> + break;
> + case UWOP_SAVE_XMM128_FAR:
> + cache->prev_xmm_addr[PEX64_UNWCODE_INFO (p[1])] =
> + save_addr
> + + 8 * extract_unsigned_integer (p + 2, 4, byte_order);
> + break;
> + case UWOP_PUSH_MACHFRAME:
> + if (PEX64_UNWCODE_INFO (p[1]) == 0)
> + {
> + cache->prev_rip_addr = cur_sp + 0;
> + cache->prev_rsp_addr = cur_sp + 24;
> + cur_sp += 40;
> + }
> + else if (PEX64_UNWCODE_INFO (p[1]) == 1)
> + {
> + cache->prev_rip_addr = cur_sp + 8;
> + cache->prev_rsp_addr = cur_sp + 32;
> + cur_sp += 48;
> + }
> + else
> + return;
> + break;
> + default:
> + return;
> + }
> + }
> +
> + /* Adjust with the length of the opcode. */
> + switch (PEX64_UNWCODE_CODE (p[1]))
> + {
> + case UWOP_PUSH_NONVOL:
> + case UWOP_ALLOC_SMALL:
> + case UWOP_SET_FPREG:
> + case UWOP_PUSH_MACHFRAME:
> + break;
> + case UWOP_ALLOC_LARGE:
> + if (PEX64_UNWCODE_INFO (p[1]) == 0)
> + p += 2;
> + else if (PEX64_UNWCODE_INFO (p[1]) == 1)
> + p += 4;
> + else
> + return;
> + break;
> + case UWOP_SAVE_NONVOL:
> + case UWOP_SAVE_XMM128:
> + p += 2;
> + break;
> + case UWOP_SAVE_NONVOL_FAR:
> + case UWOP_SAVE_XMM128_FAR:
> + p += 4;
> + break;
> + default:
> + return;
> + }
> + }
> + if (PEX64_UWI_FLAGS (ex_ui.Version_Flags) != UNW_FLAG_CHAININFO)
> + break;
> + else
> + {
> + /* Read the chained unwind info. */
> + struct external_pex64_runtime_function d;
> + CORE_ADDR chain_vma;
> +
> + chain_vma = cache->image_base + unwind_info
> + + sizeof (ex_ui) + ((codes_count + 1) & ~1) * 2 + 8;
> +
> + if (target_read_memory (chain_vma, (char *) &d, sizeof (d)) != 0)
> + return;
> +
> + cache->start_address =
> + extract_unsigned_integer (d.rva_BeginAddress, 4, byte_order);
> + unwind_info =
> + extract_unsigned_integer (d.rva_EndAddress, 4, byte_order);
> + }
> +
> + /* Allow the user to break this loop. */
> + if (quit_flag)
> + return;
> + }
> + /* PC is saved by the call. */
> + if (cache->prev_rip_addr == 0)
> + cache->prev_rip_addr = cur_sp;
> + cache->prev_sp = cur_sp + 8;
> +
> + if (unwind_debug)
> + fprintf_unfiltered (gdb_stdlog, " prev_sp: %s, prev_pc @%s\n",
> + paddress (gdbarch, cache->prev_sp),
> + paddress (gdbarch, cache->prev_rip_addr));
> +}
> +
> +static struct x64_frame_cache *
> +x64_frame_cache (struct frame_info *this_frame, void **this_cache)
> +{
> + struct gdbarch *gdbarch = get_frame_arch (this_frame);
> + enum bfd_endian byte_order = gdbarch_byte_order (gdbarch);
> + struct x64_frame_cache *cache;
> + char buf[8];
> + struct obj_section *sec;
> + pe_data_type *pe;
> + IMAGE_DATA_DIRECTORY *dir;
> + CORE_ADDR image_base;
> + CORE_ADDR pc;
> + struct objfile *objfile;
> + unsigned long lo, hi;
> + CORE_ADDR unwind_info;
> +
> + if (*this_cache)
> + return *this_cache;
> +
> + cache = FRAME_OBSTACK_ZALLOC (struct x64_frame_cache);
> + *this_cache = cache;
> +
> + /* Get current PC and SP. */
> + pc = get_frame_pc (this_frame);
> + get_frame_register (this_frame, AMD64_RSP_REGNUM, buf);
> + cache->sp = extract_unsigned_integer (buf, 8, byte_order);
> + cache->pc = pc;
> +
> + /* Get the corresponding exception directory. */
> + sec = find_pc_section (pc);
> + if (unwind_debug)
> + fprintf_unfiltered (gdb_stdlog, "x64_frame_cache: pc=%s, sp=%s, sec=%p\n",
> + paddress (gdbarch, pc),
> + paddress (gdbarch, cache->sp), sec);
> + if (sec == NULL)
> + return cache;
> + objfile = sec->objfile;
> + pe = pe_data (sec->objfile->obfd);
> + dir = &pe->pe_opthdr.DataDirectory[PE_EXCEPTION_TABLE];
> +
> + image_base = pe->pe_opthdr.ImageBase
> + + ANOFFSET (objfile->section_offsets, SECT_OFF_TEXT (objfile));
> + cache->image_base = image_base;
> +
> + if (unwind_debug)
> + fprintf_unfiltered (gdb_stdlog,
> + "x64_frame_cache: file:%s, image_base=%s\n",
> + objfile->name, paddress (gdbarch, image_base));
> +
> + /* Find the entry.
> + Note: it might be a better idea to ask directly to the kernel. This
> + will handle dynamically added entries (for JIT engines). */
> + lo = 0;
> + hi = dir->Size / sizeof (struct external_pex64_runtime_function);
> + unwind_info = 0;
> + while (lo <= hi)
> + {
> + unsigned long mid = lo + (hi - lo) / 2;
> + struct external_pex64_runtime_function d;
> + CORE_ADDR sa, ea;
> +
> + if (target_read_memory
> + (image_base + dir->VirtualAddress + mid * sizeof (d),
> + (char *) &d, sizeof (d)) != 0)
> + return cache;
> +
> + sa = extract_unsigned_integer (d.rva_BeginAddress, 4, byte_order);
> + ea = extract_unsigned_integer (d.rva_EndAddress, 4, byte_order);
> + if (pc < image_base + sa)
> + hi = mid - 1;
> + else if (pc >= image_base + ea)
> + lo = mid + 1;
> + else if (pc >= image_base + sa && pc < image_base + ea)
> + {
> + /* Got it. */
> + cache->start_address = sa;
> + unwind_info =
> + extract_unsigned_integer (d.rva_UnwindData, 4, byte_order);
> + break;
> + }
> + else
> + break;
> + }
> +
> + if (unwind_debug)
> + fprintf_unfiltered (gdb_stdlog,
> + "x64_frame_cache: image_base=%s, unwind_data=%s\n",
> + paddress (gdbarch, cache->image_base),
> + paddress (gdbarch, unwind_info));
> +
> + if (unwind_info == 0)
> + {
> + /* Assume a leaf function. */
> + cache->prev_sp = cache->sp + 8;
> + cache->prev_rip_addr = cache->sp;
> + }
> + else
> + {
> + if (unwind_info & 1)
> + {
> + /* Unofficially documented unwind info redirection. */
> + struct external_pex64_runtime_function d;
> + CORE_ADDR sa, ea;
> +
> + if (target_read_memory (image_base + (unwind_info & ~1),
> + (char *) &d, sizeof (d)) != 0)
> + return cache;
> +
> + cache->start_address =
> + extract_unsigned_integer (d.rva_BeginAddress, 4, byte_order);
> + unwind_info =
> + extract_unsigned_integer (d.rva_EndAddress, 4, byte_order);
> + }
> +
> + /* Decode unwind insns to compute saved addresses. */
> + x64_frame_decode_insns (this_frame, cache, unwind_info);
> + }
> + return cache;
> +}
> +
> +static struct value *
> +x64_frame_prev_register (struct frame_info *this_frame,
> + void **this_cache, int regnum)
> +{
> + struct gdbarch *gdbarch = get_frame_arch (this_frame);
> + enum bfd_endian byte_order = gdbarch_byte_order (gdbarch);
> + struct x64_frame_cache *cache = x64_frame_cache (this_frame, this_cache);
> + struct value *val;
> + CORE_ADDR prev;
> +
> + if (unwind_debug)
> + fprintf_unfiltered (gdb_stdlog, "x64_frame_prev_register %s for sp=%s\n",
> + gdbarch_register_name (gdbarch, regnum),
> + paddress (gdbarch, cache->prev_sp));
> +
> + if (regnum >= AMD64_XMM0_REGNUM && regnum <= AMD64_XMM0_REGNUM + 15)
> + prev = cache->prev_xmm_addr[regnum - AMD64_XMM0_REGNUM];
> + else if (regnum == AMD64_RSP_REGNUM)
> + {
> + prev = cache->prev_rsp_addr;
> + if (prev == 0)
> + return frame_unwind_got_constant (this_frame, regnum, cache->prev_sp);
> + }
> + else if (regnum >= AMD64_RAX_REGNUM && regnum <= AMD64_R15_REGNUM)
> + prev = cache->prev_reg_addr[regnum - AMD64_RAX_REGNUM];
> + else if (regnum == AMD64_RIP_REGNUM)
> + prev = cache->prev_rip_addr;
> + else
> + prev = 0;
> +
> + if (prev && unwind_debug)
> + fprintf_unfiltered (gdb_stdlog, " -> at %s\n", paddress (gdbarch, prev));
> +
> + if (prev)
> + return frame_unwind_got_memory (this_frame, regnum, prev);
> + else
> + return frame_unwind_got_register (this_frame, regnum, regnum);
> +}
> +
> +static void
> +x64_frame_this_id (struct frame_info *this_frame, void **this_cache,
> + struct frame_id *this_id)
> +{
> + struct gdbarch *gdbarch = get_frame_arch (this_frame);
> + struct x64_frame_cache *cache = x64_frame_cache (this_frame, this_cache);
> +
> + *this_id = frame_id_build (cache->prev_sp,
> + cache->image_base + cache->start_address);
> +}
> +
> +/* x64 SEH unwinder. */
> +
> +static const struct frame_unwind x64_frame_unwind =
> +{
> + NORMAL_FRAME,
> + default_frame_unwind_stop_reason,
> + &x64_frame_this_id,
> + &x64_frame_prev_register,
> + NULL,
> + default_frame_sniffer
> +};
>
> static void
> amd64_windows_init_abi (struct gdbarch_info info, struct gdbarch *gdbarch)
> @@ -174,6 +762,8 @@ amd64_windows_init_abi (struct gdbarch_info info, struct gdbarch *gdbarch)
> set_gdbarch_return_value (gdbarch, amd64_windows_return_value);
> set_gdbarch_skip_main_prologue (gdbarch, amd64_skip_main_prologue);
>
> + frame_unwind_prepend_unwinder (gdbarch, &x64_frame_unwind);
> +
> set_solib_ops (gdbarch, &solib_target_so_ops);
> }
>
>
>
>