elf

Why is ELF's .init_array section read-write?


When an ELF file needs to perform actions before main begins executing, this is encoded in a section called .init_array as an array of function pointers to be called.

For some reason, this section is marked as writable. But there does not appear to be any reason for anyone to write to that section. Why is this section not read-only?


Solution

  • But there does not appear to be any reason for anyone to write to that section.

    Let's look at at the section details of a well-known system program:

    $ readelf -SW /usr/bin/cat
    There are 31 section headers, starting at offset 0x9218:
    
    Section Headers:
      [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
      [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
      [ 1] .interp           PROGBITS        0000000000000318 000318 00001c 00   A  0   0  1
      [ 2] .note.gnu.property NOTE            0000000000000338 000338 000030 00   A  0   0  8
      [ 3] .note.gnu.build-id NOTE            0000000000000368 000368 000024 00   A  0   0  4
      [ 4] .note.ABI-tag     NOTE            000000000000038c 00038c 000020 00   A  0   0  4
      [ 5] .gnu.hash         GNU_HASH        00000000000003b0 0003b0 000024 00   A  6   0  8
      [ 6] .dynsym           DYNSYM          00000000000003d8 0003d8 000660 18   A  7   1  8
      [ 7] .dynstr           STRTAB          0000000000000a38 000a38 00033c 00   A  0   0  1
      [ 8] .gnu.version      VERSYM          0000000000000d74 000d74 000088 02   A  6   0  2
      [ 9] .gnu.version_r    VERNEED         0000000000000e00 000e00 000090 00   A  7   1  8
      [10] .rela.dyn         RELA            0000000000000e90 000e90 000288 18   A  6   0  8
      [11] .rela.plt         RELA            0000000000001118 001118 000528 18  AI  6  25  8
      [12] .init             PROGBITS        0000000000002000 002000 00001b 00  AX  0   0  4
      [13] .plt              PROGBITS        0000000000002020 002020 000380 10  AX  0   0 16
      [14] .plt.got          PROGBITS        00000000000023a0 0023a0 000010 10  AX  0   0 16
      [15] .plt.sec          PROGBITS        00000000000023b0 0023b0 000370 10  AX  0   0 16
      [16] .text             PROGBITS        0000000000002720 002720 003d82 00  AX  0   0 16
      [17] .fini             PROGBITS        00000000000064a4 0064a4 00000d 00  AX  0   0  4
      [18] .rodata           PROGBITS        0000000000007000 007000 000de8 00   A  0   0 32
      [19] .eh_frame_hdr     PROGBITS        0000000000007de8 007de8 0000c4 00   A  0   0  4
      [20] .eh_frame         PROGBITS        0000000000007eb0 007eb0 000368 00   A  0   0  8
      [21] .init_array       INIT_ARRAY      0000000000009a90 008a90 000008 08  WA  0   0  8
      [22] .fini_array       FINI_ARRAY      0000000000009a98 008a98 000008 08  WA  0   0  8
      [23] .data.rel.ro      PROGBITS        0000000000009aa0 008aa0 000148 00  WA  0   0 32
      [24] .dynamic          DYNAMIC         0000000000009be8 008be8 0001f0 10  WA  7   0  8
      [25] .got              PROGBITS        0000000000009dd8 008dd8 000220 08  WA  0   0  8
      [26] .data             PROGBITS        000000000000a000 009000 000068 00  WA  0   0 16
      [27] .bss              NOBITS          000000000000a080 009068 000140 00  WA  0   0 32
      [28] .gnu_debugaltlink PROGBITS        0000000000000000 009068 000049 00      0   0  1
      [29] .gnu_debuglink    PROGBITS        0000000000000000 0090b4 000034 00      0   0  4
      [30] .shstrtab         STRTAB          0000000000000000 0090e8 00012f 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
      L (link order), O (extra OS processing required), G (group), T (TLS),
      C (compressed), x (unknown), o (OS specific), E (exclude),
      D (mbind), l (large), p (processor specific)
      
    

    The writable sections are:

      [21] .init_array       INIT_ARRAY      0000000000009a90 008a90 000008 08  WA  0   0  8
      [22] .fini_array       FINI_ARRAY      0000000000009a98 008a98 000008 08  WA  0   0  8
      [23] .data.rel.ro      PROGBITS        0000000000009aa0 008aa0 000148 00  WA  0   0 32
      [24] .dynamic          DYNAMIC         0000000000009be8 008be8 0001f0 10  WA  7   0  8
      [25] .got              PROGBITS        0000000000009dd8 008dd8 000220 08  WA  0   0  8
      [26] .data             PROGBITS        000000000000a000 009000 000068 00  WA  0   0 16
      [27] .bss              NOBITS          000000000000a080 009068 000140 00  WA  0   0 32
    

    We know why someone might want to write to .data (writable data) and .bss (statically initialized writable data), but why would anyone want to write to any of the .init_array, .fini_array, .data.rel.ro, .dynamic or .got sections?

    Nobody normally does, but in a DSO or ordinary program (dynamically linked, PIE) the dynamic linker wants the ability to:


    That's why these sections are writable. But it doesn't follow that you can write a program that writes to any of them. (At least not with default linkage).

    Let's see some more information about /usr/bin/cat, this time the program headers:

    $ readelf -lW /usr/bin/cat
    
    Elf file type is DYN (Position-Independent Executable file)
    Entry point 0x3ac0
    There are 13 program headers, starting at offset 64
    
    Program Headers:
      Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
      PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R   0x8
      INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
          [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
      LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x001640 0x001640 R   0x1000
      LOAD           0x002000 0x0000000000002000 0x0000000000002000 0x0044b1 0x0044b1 R E 0x1000
      LOAD           0x007000 0x0000000000007000 0x0000000000007000 0x001218 0x001218 R   0x1000
      LOAD           0x008a90 0x0000000000009a90 0x0000000000009a90 0x0005d8 0x000730 RW  0x1000
      DYNAMIC        0x008be8 0x0000000000009be8 0x0000000000009be8 0x0001f0 0x0001f0 RW  0x8
      NOTE           0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
      NOTE           0x000368 0x0000000000000368 0x0000000000000368 0x000044 0x000044 R   0x4
      GNU_PROPERTY   0x000338 0x0000000000000338 0x0000000000000338 0x000030 0x000030 R   0x8
      GNU_EH_FRAME   0x007de8 0x0000000000007de8 0x0000000000007de8 0x0000c4 0x0000c4 R   0x4
      GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
      GNU_RELRO      0x008a90 0x0000000000009a90 0x0000000000009a90 0x000570 0x000570 R   0x1
    
     Section to Segment mapping:
      Segment Sections...
       00     
       01     .interp 
       02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
       03     .init .plt .plt.got .plt.sec .text .fini 
       04     .rodata .eh_frame_hdr .eh_frame 
       05     .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 
       06     .dynamic 
       07     .note.gnu.property 
       08     .note.gnu.build-id .note.ABI-tag 
       09     .note.gnu.property 
       10     .eh_frame_hdr 
       11     
       12     .init_array .fini_array .data.rel.ro .dynamic .got
    

    You see that final memory segment GNU_RELRO? "RELRO" there stands for something like "relocated readonly". It is marked R ( = readonly). It is not a real (LOAD) segment; it's a virtual segment. You see it has the same image base address, 0x8a90. as LOAD segment #05, which is RW (read/write), but LOAD segment #5 is 0x730 bytes long while GNU_RELRO is 0x570 bytes, 0x1C0 bytes shorter.

    The GNU_RELRO segment is just an initial sub-segment of LOAD segment #5 that has been marked readonly to the kernel by an mprotect call. So it doesn't count at runtime that LOAD segment #5 is all RW : the initial 0x570 bytes of it are readonly, because they're inside the GNU_RELRO segment. The final 0x1C0 bytes of LOAD segment #5 are still RW .

    This is the means by which dynamic linker can edit sections that are destined to be readonly in the process. It does the edits on those sections, and as soon as it's finished it mprotects them all, so no further writing is possible.

    The Section to Segment mapping tells us that the sections mapped into LOAD segment #5 are:

       05     .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 
       
    

    and the ones mapped into GNU_RELRO (#12) are:

       12     .init_array .fini_array .data.rel.ro .dynamic .got
       
    

    The LOAD segment #5 sections that are left writable are .data and .bss.

    The static linker co-operates in this mechanism, per it's default linker script, by laying out sections in the image so that the all the ones the GNU_RELRO segment needs to span are consecutive.

    So if you were to write a program that tried to re-write its own .init_array, the dynamic linker would relocate the entries in that array as usual, but when your program attempted to write to it (even in a pre-main initialisation function) it would segfault with a memory protection error.

    That's per the default behaviour of the linker. If for some reason you really wanted to write a program that could itself write to sections that get GNU_RELRO protection by default, you can link it with the options -Wl,-z,norelro, and then the GNU_RELRO segment just won't exist.


    Optional reading. The rest is only interesting if you'd like to see all that unfolding live in gdb

    Here are the source files of a C program that wants to decide at runtime which pre-main initialisation functions will be registered in the .init_array, so it needs to write to the .init_array. It's a silly program - the same functional effect would be achieved without touching the .init_array - and it could be coded more concisely in just one source file, but splitting it up yields a more helpful linkage map.

    $ tail -n +1 *.h *.c
    ==> decls.h <==
    #ifndef DECLS_H
    #define DECLS_H
    
    typedef void (*pfunc_t)();
    
    __attribute__((constructor(101),visibility("hidden")))
    extern void init(int argc, char *argv[]);
    
    __attribute__((visibility("hidden")))
    extern void foo_init();
    
    __attribute__((visibility("hidden")))
    extern void bar_init();
    
    __attribute__((visibility("hidden")))
    extern void default_opt_init();
    
    __attribute__((section(".init_array"),visibility("hidden")))
    extern pfunc_t opt_init;
    
    #endif
    
    ==> bar_init.c <==
    #include "decls.h"
    #include <stdio.h>
    
    void bar_init() {
        printf("%s()\n",__func__);
    }
    
    ==> default_opt_init.c <==
    #include "decls.h"
    #include <stdio.h>
    
    void default_opt_init() {
        printf("%s()\n",__func__);
    }
    
    ==> foo_init.c <==
    #include "decls.h"
    #include <stdio.h>
    
    void foo_init() {
        printf("%s()\n",__func__);
    }
    
    ==> init.c <==
    #define GNU_SOURCE
    #include "decls.h"
    #include <stdio.h>
    #include <string.h>
    #include <assert.h>
    
    extern pfunc_t __init_array_start;
    extern pfunc_t __init_array_end;
    
    static char const * which_initor(pfunc_t pinitor) {
        assert(pinitor);
        if (pinitor == &default_opt_init) {
            return "default_opt_init";
        }
        if (pinitor == (pfunc_t)&init) {
            return "init";
        }
        if (pinitor == &foo_init) {
            return "foo_init";
        }
        if (pinitor == &bar_init) {
            return "bar_init";
        }
        return "<Unidentified>";
    }
    
    static void report_init_array(void)
    {
        printf(".init_array starts at 0x%lx and ends at 0x%lx\n",
            (size_t)&__init_array_start,(size_t)&__init_array_end);
        size_t ninitors = &__init_array_end - &__init_array_start;
        printf("There are %lu entries in .init_array\n",ninitors);
        printf("Which are {\n");
        unsigned i = 0;
        for (pfunc_t *ppinitor = &__init_array_start; 
            ppinitor < &__init_array_end; ++ppinitor,++i) {
                printf("[%u] 0x%lx (%s)\n",
                i,(size_t)*ppinitor,which_initor(*ppinitor));
        }
        printf("}\n");
    }
    
    static void insert_opt_initor(char const *name, pfunc_t pinitor)
    {
        printf("Setting .init_array[%ld] = %s\n",
            &opt_init - &__init_array_start,name);
        opt_init = pinitor;
        report_init_array();
    }
    
    void init(int argc, char *argv[])
    {
        printf("%s(argc=%d argv=0x%p)\n",__func__,argc,(void *)argv);
        report_init_array();
        if (argc != 2) {
            return;
        }
        if (0 == strcmp(argv[1],"foo_init")) {
            insert_opt_initor("foo_init",&foo_init);
            return;
        }
        if (0 == strcmp(argv[1],"bar_init")) {
            insert_opt_initor("bar_init",&bar_init);
            return;
        }
        return;
    }
    
    
    ==> main.c <==
    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
        printf("%s(argc=%d argv=0x%p)\n",__func__,argc,(void *)argv);
        return 0;
    }
    
    ==> opt_init.c <==
    #include "decls.h"
    
    pfunc_t opt_init = &default_opt_init;
    

    The program has one pre-main initialisation function init with maximum initialisation priority = 101, which means the address of init is registered first in the .init_array. init receives the commandline arguments argc, argv before main does. (Because any GNU initialization function can receive the argc, argv. Nothing to do with initialisation priority.)

    The program registers a second initialisation function pointer opt_init in the .init_array which takes lower (default) priority than 101 and is initialised to point to the function default_opt_init. This function will therefore be the second pre-main initialiser to run after init by default. But if the name "foo_init" or "bar_init" is passed to init in argv[1] then the address of the function with that name will be written into the .init_array at the address opt_init and will be run instead of default_opt_init.

    So the first initialisation function decides based on the commandline arguments whether the second initialisation function will be default_opt_init, foo_init or bar_init either by writing the address of (foo|bar)_init in the opt_init slot in .init_array, or else leaving .init_array untouched.

    Or that's the plan.

    Compile and link:

    $ gcc --version | head -n1
    gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
    
    $ ls *.c
    bar_init.c  default_opt_init.c  foo_init.c  init.c  main.c  opt_init.c
    
    $ $ gcc -g -c *.c -Wall -Wextra -pedantic
    
    $ ls *.o
    bar_init.o  default_opt_init.o  foo_init.o  init.o  main.o  opt_init.o
    
    $ gcc -o prog *.o -Wl,-Map=mapfile
    

    The mapfile shows:

    $ grep -A4 -P '(^|\W).init_array' mapfile
    .init_array     0x0000000000003d90       0x18
                    0x0000000000003d90                PROVIDE (__init_array_start = .)
     *(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*))
     .init_array.00101
                    0x0000000000003d90        0x8 init.o
     *(.init_array EXCLUDE_FILE(*crtend?.o *crtend.o *crtbegin?.o *crtbegin.o) .ctors)
     .init_array    0x0000000000003d98        0x8 /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o
     .init_array    0x0000000000003da0        0x8 opt_init.o
                    0x0000000000003da0                opt_init
                    0x0000000000003da8                PROVIDE (__init_array_end = .)
    
    .fini_array     0x0000000000003da8        0x8
    

    that the .init_array output section starts at offset 0x3d90 ( = __init_array_start ) in the image and has 3 0x8 byte (pointer-size) input sections coming from:

    ending at 0x3da8 ( = __init_array_end), which is also the start of the .fini_array.

    init.o provides our:

    __attribute__((constructor(101),visibility("hidden")))
    void init(int argc, char *argv[]);
    

    and opt_init.o provides our:

    __attribute__((section(".init_array"),visibility("hidden")))
    pfunc_t opt_init = &default_opt_init;
    

    init is sorted into first position in the array because I gave it maximum initialisation priority 101 and the linker sorts the output section:

    SORT_BY_INIT_PRIORITY(.init_array.*)
    

    Entries with no specified initialisation priority get the default 65535 (lowest priority).

    Now let's run the program under gdb, giving argument "foo_init" so that the second initialisation function to run will be foo_init instead of default_opt_init:

    $ gdb --args prog "foo_init"
    ...[cut preamble]...
    Reading symbols from prog...
    

    We'll have initial breakpoints on all of:

    (gdb) b _start
    Breakpoint 1 at 0x10c0
    (gdb) b init
    Breakpoint 2 at 0x1237: file init.c, line 53.
    (gdb) b insert_opt_initor
    Breakpoint 3 at 0x148c: file init.c, line 46.
    (gdb) b main
    Breakpoint 4 at 0x14eb: file main.c, line 5.
    

    Before we run, let's see the unrelocatad addresses of our pre-registered initialisation functions init and default_opt_init and the .init_array itself.

    (gdb) p/x &init
    $2 = 0x1224
    (gdb) p/x &default_opt_init
    $3 = 0x11d2
    (gdb) p/x &__init_array_start
    $4 = 0x3d90
    

    All as per the mapfile. Run:

    (gdb) r
    Starting program: /home/imk/develop/so/scrap/prog foo_init
    
    Breakpoint 1.2, 0x00007ffff7fe4540 in _start () from /lib64/ld-linux-x86-64.so.2
    

    Entered our program. By now __init_array_start has already been relocated:

    (gdb) p/x &__init_array_start
    $2 = 0x555555557d90
    

    &__init_array_start is a pointer-to-pointer-to-function. Let's see the current contents of elements #0 and #2:

    (gdb) p/x ((void (**)())(&__init_array_start))[0]
    $9 = 0x1224
    (gdb) p/x ((void (**)())(&__init_array_start))[2]
    $10 = 0x11d2
    

    They're still holding the unrelocated addresses we've just seen for our init and default_opt_init. Let's put watchpoints on elements #0 and #2:

    (gdb) watch ((void (**)())(&__init_array_start))[0]
    Hardware watchpoint 5: ((void (**)())(&__init_array_start))[0]
    (gdb) watch ((void (**)())(&__init_array_start))[2]
    Hardware watchpoint 6: ((void (**)())(&__init_array_start))[2]
    

    and carry on:

    (gdb) c
    Continuing.
    
    Hardware watchpoint 5: ((void (**)())(&__init_array_start))[0]
    
    Old value = (void (*)()) 0x1224
    New value = (void (*)()) 0x555555555224 <init>  
    elf_dynamic_do_Rela (skip_ifunc=<optimised out>, lazy=0, nrelative=5, relsize=336, reladdr=93824992232888, scope=<optimised out>, map=0x7ffff7ffe2e0) at ./elf/do-rel.h:123
    warning: 123    ./elf/do-rel.h: No such file or directory
    

    Now we're executing in the dynamic linker at elf_dynamic_do_Rela, doing dynamic relocations in our .init_array and have just written element #0, setting it to the runtime address of init.

    (gdb) c
    Continuing.
    
    Hardware watchpoint 6: ((void (**)())(&__init_array_start))[2]
    
    Old value = (void (*)()) 0x11d2
    New value = (void (*)()) 0x5555555551d2 <default_opt_init>
    elf_dynamic_do_Rela (skip_ifunc=<optimised out>, lazy=0, nrelative=5, relsize=336, reladdr=93824992232888, scope=<optimised out>, map=0x7ffff7ffe2e0) at ./elf/do-rel.h:123
    123 in ./elf/do-rel.h
    

    And that's the write to element #2, setting it to the runtime address of default_opt_init.

    Now let's put a breakpoint on mprotect, to catch the GNU_RELRO segment being set up:

    (gdb) b mprotect
    Breakpoint 7 at 0x7ffff7feadb0: file ../sysdeps/unix/syscall-template.S, line 117.
    
    (gdb) c
    Continuing.
    
    Breakpoint 7, __GI_mprotect () at ../sysdeps/unix/syscall-template.S:117
    warning: 117    ../sysdeps/unix/syscall-template.S: No such file or directory
    
    (gdb) info reg
    rax            0x1000              4096
    rbx            0x0                 0
    rcx            0x555555557d90      93824992247184
    rdx            0x1                 1
    rsi            0x1000              4096
    rdi            0x555555557000      93824992243712
    ...[cut]...
    

    That's GNU_RELRO being enacted. Remember rcx = 0x555555557d90 is the address &__init_array_start. rax = 0x1000 is the size ( = 1 page) to be protected. rdi = 0x555555557000 is the nearest preceding page boundary where protection can start. There'll more mprotect calls to follow that are irrelevant to us, so we'll stop watching them:

    (gdb) info break
    Num     Type           Disp Enb Address            What
    1       breakpoint     keep y   <MULTIPLE>         
        breakpoint already hit 1 time
    1.1                         y   0x00005555555550c0 <_start>
    1.2                         y   0x00007ffff7fe4540 <_start>
    2       breakpoint     keep y   0x0000555555555237 in init at init.c:53
    3       breakpoint     keep y   0x000055555555548c in insert_opt_initor at init.c:46
    4       breakpoint     keep y   0x00005555555554eb in main at main.c:5
    5       hw watchpoint  keep y                      ((void (**)())(&__init_array_start))[0]
        breakpoint already hit 1 time
    6       hw watchpoint  keep y                      ((void (**)())(&__init_array_start))[2]
        breakpoint already hit 1 time
    7       breakpoint     keep y   0x00007ffff7feadb0 in mprotect at ../sysdeps/unix/syscall-template.S:117
        breakpoint already hit 1 time
    (gdb) del 7
    

    and carry on:

    (gdb) c
    Continuing.
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    
    Breakpoint 1.1, 0x00005555555550c0 in _start ()
    

    We've left the dynamic linker and come back to our program:

    (gdb) c
    Continuing.
    
    Breakpoint 2.1, init (argc=2, argv=0x7fffffffdd98) at init.c:53
    53      printf("%s(argc=%d argv=0x%p)\n",__func__,argc,(void *)argv);
    

    Now stopped at our init:

    (gdb) c
    Continuing.
    init(argc=2 argv=0x0x7fffffffdd98)
    .init_array starts at 0x555555557d90 and ends at 0x555555557da8
    There are 3 entries in .init_array
    Which are {
    [0] 0x555555555224 (init)
    [1] 0x5555555551a0 (<Unidentified>)
    [2] 0x5555555551d2 (default_opt_init)
    }
    
    Breakpoint 3, insert_opt_initor (name=0x555555556077 "foo_init", pinitor=0x5555555551fb <foo_init>) at init.c:46
    46          &opt_init - &__init_array_start,name);
    

    There's the first part of init's console output and we've stopped at insert_opt_initor, where we'll try to replace &default_opt_init with &foo_init in the the .init_array:

    (gdb) c
    Continuing.
    Setting .init_array[2] = foo_init
    
    Program received signal SIGSEGV, Segmentation fault.
    0x00005555555554c9 in insert_opt_initor (name=0x555555556077 "foo_init", pinitor=0x5555555551fb <foo_init>) at init.c:47
    47      opt_init = pinitor;
    

    Segfault, because the .init_array is now in GNU_RELRO.


    We can avoid that death by relinking the program to suppress creation of the GNU_RELRO segment:

    $ gcc -o prog *.o -Wl,-z,norelro
    

    Then with no arguments the program runs by default:

     ./prog
    init(argc=1 argv=0x0x7ffccf47d1d8)
    .init_array starts at 0x5ea5ab5d6388 and ends at 0x5ea5ab5d63a0
    There are 3 entries in .init_array
    Which are {
    [0] 0x5ea5ab5d4224 (init)
    [1] 0x5ea5ab5d41a0 (<Unidentified>)
    [2] 0x5ea5ab5d41d2 (default_opt_init)
    }
    default_opt_init()
    main(argc=1 argv=0x0x7ffccf47d1d8)
    

    And with this arg:

     ./prog "foo_init"
    init(argc=2 argv=0x0x7ffc27cdde88)
    .init_array starts at 0x635c204ab388 and ends at 0x635c204ab3a0
    There are 3 entries in .init_array
    Which are {
    [0] 0x635c204a9224 (init)
    [1] 0x635c204a91a0 (<Unidentified>)
    [2] 0x635c204a91d2 (default_opt_init)
    }
    Setting .init_array[2] = foo_init
    .init_array starts at 0x635c204ab388 and ends at 0x635c204ab3a0
    There are 3 entries in .init_array
    Which are {
    [0] 0x635c204a9224 (init)
    [1] 0x635c204a91a0 (<Unidentified>)
    [2] 0x635c204a91fb (foo_init)
    }
    foo_init()
    main(argc=2 argv=0x0x7ffc27cdde88)
    

    And with this arg:

    $ ./prog "bar_init"
    init(argc=2 argv=0x0x7ffc85007218)
    .init_array starts at 0x6099f0b20388 and ends at 0x6099f0b203a0
    There are 3 entries in .init_array
    Which are {
    [0] 0x6099f0b1e224 (init)
    [1] 0x6099f0b1e1a0 (<Unidentified>)
    [2] 0x6099f0b1e1d2 (default_opt_init)
    }
    Setting .init_array[2] = bar_init
    .init_array starts at 0x6099f0b20388 and ends at 0x6099f0b203a0
    There are 3 entries in .init_array
    Which are {
    [0] 0x6099f0b1e224 (init)
    [1] 0x6099f0b1e1a0 (<Unidentified>)
    [2] 0x6099f0b1e1a9 (bar_init)
    }
    bar_init()
    main(argc=2 argv=0x0x7ffc85007218)
    

    In each case running to completion.