From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: (qmail 94488 invoked by alias); 1 Jun 2015 20:58:12 -0000 Mailing-List: contact binutils-help@sourceware.org; run by ezmlm Precedence: bulk List-Id: List-Subscribe: List-Archive: List-Post: List-Help: , Sender: binutils-owner@sourceware.org Received: (qmail 94430 invoked by uid 89); 1 Jun 2015 20:58:11 -0000 Authentication-Results: sourceware.org; auth=none X-Virus-Found: No X-Spam-SWARE-Status: No, score=-0.1 required=5.0 tests=AWL,BAYES_50,KAM_LAZY_DOMAIN_SECURITY,SPF_HELO_PASS,T_RP_MATCHES_RCVD autolearn=no version=3.3.2 X-HELO: mx1.redhat.com Received: from mx1.redhat.com (HELO mx1.redhat.com) (209.132.183.28) by sourceware.org (qpsmtpd/0.93/v0.84-503-g423c35a) with (AES256-GCM-SHA384 encrypted) ESMTPS; Mon, 01 Jun 2015 20:58:06 +0000 Received: from int-mx11.intmail.prod.int.phx2.redhat.com (int-mx11.intmail.prod.int.phx2.redhat.com [10.5.11.24]) by mx1.redhat.com (Postfix) with ESMTPS id 936892DC3C2; Mon, 1 Jun 2015 20:50:16 +0000 (UTC) Received: from c64.redhat.com (vpn-230-103.phx2.redhat.com [10.3.230.103]) by int-mx11.intmail.prod.int.phx2.redhat.com (8.14.4/8.14.4) with ESMTP id t51KoE59030113; Mon, 1 Jun 2015 16:50:16 -0400 From: David Malcolm To: gcc-patches@gcc.gnu.org, binutils@sourceware.org Cc: David Malcolm Subject: [PATCH 02/16] gcc: Embed the driver in-process within libgccjit Date: Mon, 01 Jun 2015 20:58:00 -0000 Message-Id: <1433192664-50156-3-git-send-email-dmalcolm@redhat.com> In-Reply-To: <1433192664-50156-1-git-send-email-dmalcolm@redhat.com> References: <1433192664-50156-1-git-send-email-dmalcolm@redhat.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-IsSubscribed: yes X-SW-Source: 2015-06/txt/msg00021.txt.bz2 Provide a way to clean up state within the driver code, and use this from libgccjit to embed it in-process, rather that via pex. Part of this requires restoring the environment after any putenv calls, so the patch introduces an env_manager class. No effort is made to restore the environment for the classic use-case. This embedding gives a slight performance win for jit.dg/test-benchmark.c, and enables bigger performance gains in followup patches. I've fixed the worst of the memory leaks, but this does still leak somewhat. gcc/ChangeLog: * gcc-main.c (main): Add params to driver ctor. * gcc.c (class env_manager): New. (env): New global. (env_manager::init): New. (env_manager::getenv): New. (env_manager::xputenv): New. (env_manager::restore): New. Poison genenv and putenv. (DEFAULT_TARGET_SYSTEM_ROOT): New. (target_system_root): Update initialization to use DEFAULT_TARGET_SYSTEM_ROOT. (struct spec_list): Add field "default_ptr". (INIT_STATIC_SPEC): Initialize new field "default_ptr". (init_spec): Likewise. (set_spec): Clear field "default_ptr". (read_specs): Free "spec" and "buffer". (xputenv): Reimplement in terms of env_manager. (process_command): Replace ::getenv calls with calls to the env_manager singleton. (process_brace_body): Free string in three places. (driver::driver): New. (driver::~driver): New. (used_arg): Convert from a function to... (class used_arg_t): ...this class, and... (used_arg): ...this new global instance. (used_arg_t::finalize): New function. (getenv_spec_function): Add "const" to local "value". Replace ::getenv call with call to the env_manager singleton. (path_prefix_reset): New function. (driver::finalize): New function. * gcc.h (driver::driver): New. (driver::~driver): New. (driver::finalize): New. gcc/jit/ChangeLog: * jit-playback.c (gcc_driver_name): New global. (gcc:jit::playback::context::invoke_driver): Split out second half into... (gcc::jit::playback::context::invoke_embedded_driver): ...this new function, and... (gcc::jit::playback::context::invoke_external_driver): ...this new function. * jit-playback.h (gcc::jit::playback::context::invoke_embedded_driver): New. (gcc::jit::playback::context::invoke_external_driver): New. * notes.txt: Show invocation of embedded copy of driver. --- gcc/gcc-main.c | 3 +- gcc/gcc.c | 392 ++++++++++++++++++++++++++++++++++++++++++++++--- gcc/gcc.h | 3 + gcc/jit/jit-playback.c | 49 ++++++- gcc/jit/jit-playback.h | 8 + gcc/jit/notes.txt | 8 +- 6 files changed, 432 insertions(+), 31 deletions(-) diff --git a/gcc/gcc-main.c b/gcc/gcc-main.c index 230ba48..a0aaa3c 100644 --- a/gcc/gcc-main.c +++ b/gcc/gcc-main.c @@ -40,7 +40,8 @@ extern int main (int, char **); int main (int argc, char **argv) { - driver d; + driver d (false, /* can_finalize */ + false); /* debug */ return d.main (argc, argv); } diff --git a/gcc/gcc.c b/gcc/gcc.c index 8f01e42..683392d 100644 --- a/gcc/gcc.c +++ b/gcc/gcc.c @@ -44,6 +44,127 @@ compilation is specified by a string called a "spec". */ #include "vec.h" #include "filenames.h" + + +/* Manage the manipulation of env vars. We poison getenv and putenv, so that + all enviroment-handling is done through this class. */ + +class env_manager +{ + public: + void init (bool can_restore, bool debug); + const char *getenv (const char *name); + void xputenv (const char *string); + void restore (); + + private: + bool m_can_restore; + bool m_debug; + struct kv + { + char *m_key; + char *m_value; + }; + vec m_keys; + +}; + +/* The singleton instance of class env_manager. */ + +static env_manager env; + +/* Initializer for class env_manager. + + We can't do this as a constructor since we have a statically + allocated instance ("env" above). */ + +void +env_manager::init (bool can_restore, bool debug) +{ + m_can_restore = can_restore; + m_debug = debug; +} + +/* Get the value of NAME within the environment. Essentially + a wrapper for ::getenv, but adding logging, and the possibility + of caching results. */ + +const char * +env_manager::getenv (const char *name) +{ + const char *result = ::getenv (name); + if (m_debug) + fprintf (stderr, "env_manager::getenv (%s) -> %s\n", name, result); + return result; +} + +/* Put the given KEY=VALUE entry STRING into the environment. + If the env_manager was initialized with CAN_RESTORE set, then + also record the old value of KEY within the environment, so that it + can be later restored. */ + +void +env_manager::xputenv (const char *string) +{ + if (m_debug) + fprintf (stderr, "env_manager::xputenv (%s)\n", string); + if (verbose_flag) + fnotice (stderr, "%s\n", string); + + if (m_can_restore) + { + char *equals = strchr (const_cast (string), '='); + gcc_assert (equals); + + struct kv kv; + kv.m_key = strndup (string, equals - string); + const char *cur_value = ::getenv (kv.m_key); + if (m_debug) + fprintf (stderr, "saving old value: %s\n",cur_value); + kv.m_value = cur_value ? xstrdup (cur_value) : NULL; + m_keys.safe_push (kv); + } + + ::putenv (CONST_CAST (char *, string)); +} + +/* Undo any xputenv changes made since last restore. + Can only be called if the env_manager was initialized with + CAN_RESTORE enabled. */ + +void +env_manager::restore () +{ + unsigned int i; + struct kv *item; + + gcc_assert (m_can_restore); + + FOR_EACH_VEC_ELT_REVERSE (m_keys, i, item) + { + if (m_debug) + printf ("restoring saved key: %s value: %s\n", item->m_key, item->m_value); + if (item->m_value) + { + char *newstr = concat (item->m_key, "=", item->m_value, NULL); + ::putenv (newstr); + } + else + ::unsetenv (item->m_key); + free (item->m_key); + free (item->m_value); + } + + m_keys.truncate (0); +} + +/* Forbid other uses of getenv and putenv. */ +#if (GCC_VERSION >= 3000) +#pragma GCC poison genenv putenv +#endif + + + /* By default there is no special suffix for target executables. */ /* FIXME: when autoconf is fixed, remove the host check - dj */ #if defined(TARGET_EXECUTABLE_SUFFIX) && defined(HOST_EXECUTABLE_SUFFIX) @@ -116,10 +237,11 @@ FILE *report_times_to_file = NULL; and library files can be found in an alternate location. */ #ifdef TARGET_SYSTEM_ROOT -static const char *target_system_root = TARGET_SYSTEM_ROOT; +#define DEFAULT_TARGET_SYSTEM_ROOT (TARGET_SYSTEM_ROOT) #else -static const char *target_system_root = 0; +#define DEFAULT_TARGET_SYSTEM_ROOT (0) #endif +static const char *target_system_root = DEFAULT_TARGET_SYSTEM_ROOT; /* Nonzero means pass the updated target_system_root to the compiler. */ @@ -236,7 +358,6 @@ static const char *validate_switches (const char *, bool); static void validate_all_switches (void); static inline void validate_switches_from_spec (const char *, bool); static void give_switch (int, int); -static int used_arg (const char *, int); static int default_arg (const char *, int); static void set_multilib_dir (void); static void print_multilib_info (void); @@ -1346,10 +1467,12 @@ struct spec_list int name_len; /* length of the name */ bool user_p; /* whether string come from file spec. */ bool alloc_p; /* whether string was allocated */ + const char *default_ptr; /* The default value of *ptr_spec. */ }; #define INIT_STATIC_SPEC(NAME,PTR) \ - { NAME, NULL, PTR, (struct spec_list *) 0, sizeof (NAME) - 1, false, false } + { NAME, NULL, PTR, (struct spec_list *) 0, sizeof (NAME) - 1, false, false, \ + *PTR } /* List of statically defined specs. */ static struct spec_list static_specs[] = @@ -1516,6 +1639,8 @@ init_spec (void) sl->next = next; sl->name_len = strlen (sl->name); sl->ptr_spec = &sl->ptr; + gcc_assert (sl->ptr_spec != NULL); + sl->default_ptr = sl->ptr; next = sl; } #endif @@ -1690,6 +1815,7 @@ set_spec (const char *name, const char *spec, bool user_p) sl->alloc_p = 0; *(sl->ptr_spec) = ""; sl->next = specs; + sl->default_ptr = NULL; specs = sl; } @@ -2082,7 +2208,10 @@ read_specs (const char *filename, bool main_p, bool user_p) if (! strcmp (suffix, "*link_command")) link_command_spec = spec; else - set_spec (suffix + 1, spec, user_p); + { + set_spec (suffix + 1, spec, user_p); + free (spec); + } } else { @@ -2102,6 +2231,8 @@ read_specs (const char *filename, bool main_p, bool user_p) if (link_command_spec == 0) fatal_error (input_location, "spec file has no spec for linking"); + + XDELETEVEC (buffer); } /* Record the names of temporary files we tell compilers to write, @@ -2444,9 +2575,7 @@ add_to_obstack (char *path, void *data) static void xputenv (const char *string) { - if (verbose_flag) - fnotice (stderr, "%s\n", string); - putenv (CONST_CAST (char *, string)); + env.xputenv (string); } /* Build a list of search directories from PATHS. @@ -3910,7 +4039,7 @@ process_command (unsigned int decoded_options_count, struct cl_option_handlers handlers; unsigned int j; - gcc_exec_prefix = getenv ("GCC_EXEC_PREFIX"); + gcc_exec_prefix = env.getenv ("GCC_EXEC_PREFIX"); n_switches = 0; n_infiles = 0; @@ -4015,7 +4144,7 @@ process_command (unsigned int decoded_options_count, /* COMPILER_PATH and LIBRARY_PATH have values that are lists of directory names with colons. */ - temp = getenv ("COMPILER_PATH"); + temp = env.getenv ("COMPILER_PATH"); if (temp) { const char *startp, *endp; @@ -4049,7 +4178,7 @@ process_command (unsigned int decoded_options_count, } } - temp = getenv (LIBRARY_PATH_ENV); + temp = env.getenv (LIBRARY_PATH_ENV); if (temp && *cross_compile == '0') { const char *startp, *endp; @@ -4082,7 +4211,7 @@ process_command (unsigned int decoded_options_count, } /* Use LPATH like LIBRARY_PATH (for the CMU build program). */ - temp = getenv ("LPATH"); + temp = env.getenv ("LPATH"); if (temp && *cross_compile == '0') { const char *startp, *endp; @@ -4225,7 +4354,7 @@ process_command (unsigned int decoded_options_count, if (!compare_debug) { - const char *gcd = getenv ("GCC_COMPARE_DEBUG"); + const char *gcd = env.getenv ("GCC_COMPARE_DEBUG"); if (gcd && gcd[0] == '-') { @@ -6157,7 +6286,10 @@ process_brace_body (const char *p, const char *atom, const char *end_atom, if (!have_subst) { if (do_spec_1 (string, 0, NULL) < 0) - return 0; + { + free (string); + return 0; + } } else { @@ -6173,12 +6305,16 @@ process_brace_body (const char *p, const char *atom, const char *end_atom, { if (do_spec_1 (string, 0, &switches[i].part1[hard_match_len]) < 0) - return 0; + { + free (string); + return 0; + } /* Pass any arguments this switch has. */ give_switch (i, 1); suffix_subst = NULL; } } + free (string); } return p; @@ -6887,6 +7023,19 @@ compare_files (char *cmpfile[]) return ret; } +driver::driver (bool can_finalize, bool debug) : + explicit_link_files (NULL), + decoded_options (NULL) +{ + env.init (can_finalize, debug); +} + +driver::~driver () +{ + XDELETEVEC (explicit_link_files); + XDELETEVEC (decoded_options); +} + /* driver::main is implemented as a series of driver:: method calls. */ int @@ -8100,9 +8249,13 @@ static int n_mdswitches; /* Check whether a particular argument was used. The first time we canonicalize the switches to keep only the ones we care about. */ -static int -used_arg (const char *p, int len) +class used_arg_t { + public: + int operator () (const char *p, int len); + void finalize (); + + private: struct mswitchstr { const char *str; @@ -8111,8 +8264,16 @@ used_arg (const char *p, int len) int rep_len; }; - static struct mswitchstr *mswitches; - static int n_mswitches; + mswitchstr *mswitches; + int n_mswitches; + +}; + +used_arg_t used_arg; + +int +used_arg_t::operator () (const char *p, int len) +{ int i, j; if (!mswitches) @@ -8241,6 +8402,14 @@ used_arg (const char *p, int len) return 0; } +void used_arg_t::finalize () +{ + XDELETEVEC (mswitches); + mswitches = NULL; + n_mswitches = 0; +} + + static int default_arg (const char *p, int len) { @@ -8795,7 +8964,7 @@ print_multilib_info (void) static const char * getenv_spec_function (int argc, const char **argv) { - char *value; + const char *value; char *result; char *ptr; size_t len; @@ -8803,7 +8972,7 @@ getenv_spec_function (int argc, const char **argv) if (argc != 2) return NULL; - value = getenv (argv[0]); + value = env.getenv (argv[0]); if (!value) fatal_error (input_location, "environment variable %qs not defined", argv[0]); @@ -9425,6 +9594,187 @@ convert_white_space (char *orig) return orig; } +static void +path_prefix_reset (path_prefix *prefix) +{ + struct prefix_list *iter, *next; + iter = prefix->plist; + while (iter) + { + next = iter->next; + free (const_cast (iter->prefix)); + XDELETE (iter); + iter = next; + } + prefix->plist = 0; + prefix->max_len = 0; +} + +/* Restore all state within gcc.c to the initial state, so that the driver + code can be safely re-run in-process. + + Many const char * variables are referenced by static specs (see + INIT_STATIC_SPEC above). These variables are restored to their default + values by a simple loop over the static specs. + + For other variables, we directly restore them all to their initial + values (often implicitly 0). + + Free the various obstacks in this file, along with "opts_obstack" + from opts.c. + + This function also restores any environment variables that were changed. */ + +void +driver::finalize () +{ + env.restore (); + params_c_finalize (); + diagnostic_finish (global_dc); + + is_cpp_driver = 0; + at_file_supplied = 0; + print_help_list = 0; + print_version = 0; + verbose_only_flag = 0; + print_subprocess_help = 0; + use_ld = NULL; + report_times_to_file = NULL; + target_system_root = DEFAULT_TARGET_SYSTEM_ROOT; + target_system_root_changed = 0; + target_sysroot_suffix = 0; + target_sysroot_hdrs_suffix = 0; + save_temps_flag = SAVE_TEMPS_NONE; + save_temps_prefix = 0; + save_temps_length = 0; + spec_machine = DEFAULT_TARGET_MACHINE; + greatest_status = 1; + + obstack_free (&obstack, NULL); + obstack_free (&opts_obstack, NULL); /* in opts.c */ + obstack_free (&collect_obstack, NULL); + + link_command_spec = LINK_COMMAND_SPEC; + + obstack_free (&multilib_obstack, NULL); + + user_specs_head = NULL; + user_specs_tail = NULL; + + /* Within the "compilers" vec, the fields "suffix" and "spec" were + statically allocated for the default compilers, but dynamically + allocated for additional compilers. Delete them for the latter. */ + for (int i = n_default_compilers; i < n_compilers; i++) + { + free (const_cast (compilers[i].suffix)); + free (const_cast (compilers[i].spec)); + } + XDELETEVEC (compilers); + compilers = NULL; + n_compilers = 0; + + linker_options.truncate (0); + assembler_options.truncate (0); + preprocessor_options.truncate (0); + + path_prefix_reset (&exec_prefixes); + path_prefix_reset (&startfile_prefixes); + path_prefix_reset (&include_prefixes); + + machine_suffix = 0; + just_machine_suffix = 0; + gcc_exec_prefix = 0; + gcc_libexec_prefix = 0; + md_exec_prefix = MD_EXEC_PREFIX; + md_startfile_prefix = MD_STARTFILE_PREFIX; + md_startfile_prefix_1 = MD_STARTFILE_PREFIX_1; + multilib_dir = 0; + multilib_os_dir = 0; + multiarch_dir = 0; + + XDELETEVEC (specs); + specs = 0; + for (unsigned i = 0; i < ARRAY_SIZE (static_specs); i++) + { + spec_list *sl = &static_specs[i]; + if (sl->alloc_p) + { + //free (const_cast (*(sl->ptr_spec))); + sl->alloc_p = false; + } + *(sl->ptr_spec) = sl->default_ptr; + } + extra_specs = NULL; + + processing_spec_function = 0; + + argbuf.truncate (0); + + have_c = 0; + have_o = 0; + + temp_names = NULL; + execution_count = 0; + signal_count = 0; + + temp_filename = NULL; + temp_filename_length = 0; + always_delete_queue = NULL; + failure_delete_queue = NULL; + + XDELETEVEC (switches); + switches = NULL; + n_switches = 0; + n_switches_alloc = 0; + + compare_debug = 0; + compare_debug_second = 0; + compare_debug_opt = NULL; + for (int i = 0; i < 2; i++) + { + switches_debug_check[i] = NULL; + n_switches_debug_check[i] = 0; + n_switches_alloc_debug_check[i] = 0; + debug_check_temp_file[i] = NULL; + } + + XDELETEVEC (infiles); + infiles = NULL; + n_infiles = 0; + n_infiles_alloc = 0; + + combine_inputs = false; + added_libraries = 0; + XDELETEVEC (outfiles); + outfiles = NULL; + spec_lang = 0; + last_language_n_infiles = 0; + gcc_input_filename = NULL; + input_file_number = 0; + input_filename_length = 0; + basename_length = 0; + suffixed_basename_length = 0; + input_basename = NULL; + input_suffix = NULL; + /* We don't need to purge "input_stat", just to unset "input_stat_set". */ + input_stat_set = 0; + input_file_compiler = NULL; + arg_going = 0; + delete_this_arg = 0; + this_is_output_file = 0; + this_is_library_file = 0; + this_is_linker_script = 0; + input_from_pipe = 0; + suffix_subst = NULL; + + mdswitches = NULL; + n_mdswitches = 0; + + debug_auxbase_opt = NULL; + + used_arg.finalize (); +} + /* PR jit/64810. Targets can provide configure-time default options in OPTION_DEFAULT_SPECS. The jit needs to access these, but diff --git a/gcc/gcc.h b/gcc/gcc.h index f10a103..e1abe43 100644 --- a/gcc/gcc.h +++ b/gcc/gcc.h @@ -30,7 +30,10 @@ along with GCC; see the file COPYING3. If not see class driver { public: + driver (bool can_finalize, bool debug); + ~driver (); int main (int argc, char **argv); + void finalize (); private: void set_progname (const char *argv0) const; diff --git a/gcc/jit/jit-playback.c b/gcc/jit/jit-playback.c index 7a3b7f7..6282abd 100644 --- a/gcc/jit/jit-playback.c +++ b/gcc/jit/jit-playback.c @@ -2303,6 +2303,8 @@ convert_to_dso (const char *ctxt_progname) true);/* bool run_linker */ } +static const char * const gcc_driver_name = GCC_DRIVER_NAME; + void playback::context:: invoke_driver (const char *ctxt_progname, @@ -2313,15 +2315,14 @@ invoke_driver (const char *ctxt_progname, bool run_linker) { JIT_LOG_SCOPE (get_logger ()); + + const bool embedded_driver = true; + /* Currently this lumps together both assembling and linking into TV_ASSEMBLE. */ auto_timevar assemble_timevar (get_timer (), tv_id); - const char *errmsg; auto_vec argvec; #define ADD_ARG(arg) argvec.safe_push (arg) - int exit_status = 0; - int err = 0; - const char *gcc_driver_name = GCC_DRIVER_NAME; ADD_ARG (gcc_driver_name); @@ -2344,8 +2345,10 @@ invoke_driver (const char *ctxt_progname, time. */ ADD_ARG ("-fno-use-linker-plugin"); - /* pex argv arrays are NULL-terminated. */ - ADD_ARG (NULL); + if (0) + ADD_ARG ("-v"); + +#undef ADD_ARG /* pex_one's error-handling requires pname to be non-NULL. */ gcc_assert (ctxt_progname); @@ -2354,9 +2357,40 @@ invoke_driver (const char *ctxt_progname, for (unsigned i = 0; i < argvec.length (); i++) get_logger ()->log ("argv[%i]: %s", i, argvec[i]); + if (embedded_driver) + invoke_embedded_driver (&argvec); + else + invoke_external_driver (ctxt_progname, &argvec); +} + +void +playback::context:: +invoke_embedded_driver (const vec *argvec) +{ + driver d (true, /* can_finalize */ + false); /* debug */ + int result = d.main (argvec->length (), + const_cast (argvec->address ())); + d.finalize (); + if (result) + add_error (NULL, "error invoking gcc driver"); +} + +void +playback::context:: +invoke_external_driver (const char *ctxt_progname, + vec *argvec) +{ + const char *errmsg; + int exit_status = 0; + int err = 0; + + /* pex argv arrays are NULL-terminated. */ + argvec->safe_push (NULL); + errmsg = pex_one (PEX_SEARCH, /* int flags, */ gcc_driver_name, - const_cast (argvec.address ()), + const_cast (argvec->address ()), ctxt_progname, /* const char *pname */ NULL, /* const char *outname */ NULL, /* const char *errname */ @@ -2383,7 +2417,6 @@ invoke_driver (const char *ctxt_progname, getenv ("PATH")); return; } -#undef ADD_ARG } /* Dynamically-link the built DSO file into this process, using dlopen. diff --git a/gcc/jit/jit-playback.h b/gcc/jit/jit-playback.h index afe0068..7ee326b 100644 --- a/gcc/jit/jit-playback.h +++ b/gcc/jit/jit-playback.h @@ -281,6 +281,14 @@ protected: result * dlopen_built_dso (); + private: + void + invoke_embedded_driver (const vec *argvec); + + void + invoke_external_driver (const char *ctxt_progname, + vec *argvec); + private: ::gcc::jit::recording::context *m_recording_ctxt; diff --git a/gcc/jit/notes.txt b/gcc/jit/notes.txt index e92c665..36e05cb 100644 --- a/gcc/jit/notes.txt +++ b/gcc/jit/notes.txt @@ -78,7 +78,13 @@ Client Code . Generated . libgccjit.so . . │ . . . . │ (assuming an in-memory compile): . . │ . . - . . │ . Convert assembler to DSO ("fake.so") + . . --> Convert assembler to DSO, via embedded + . . copy of driver: + . . driver::main () + . . invocation of "as" + . . invocation of "ld" + . . driver::finalize () + . . <---- . . │ . . . . │ . Load DSO (dlopen "fake.so") . . │ . . -- 1.8.5.3