main is the application entry point called by the C runtime, while the kernel recognizes the ELF entry address. Typically, the startup object linked into the program defines _start, but the name itself holds no magic: the linker can specify a different entry, and programs not using libc can implement their own startup.
When execve succeeds, it replaces the current process's program image without creating a new PID, nor does it return to the next instruction of the old program. To understand the startup path, one must first distinguish between the kernel establishing the image and the user-space initialization of execution code. execve(2)
Who does the kernel hand control to?
The kernel establishes the mapping for the main image based on PT_LOAD, sets up the initial stack, and provides arguments, environment, and auxv. If PT_INTERP exists, it also loads the specified ELF interpreter and enters the interpreter entry first; otherwise, it starts from the main image's entry. The entry's runtime address must be combined with the load offset; e_entry cannot be treated as a file offset.
The dynamic interpreter is typically ld-linux from glibc. It needs to complete its own limited bootstrap and corresponding relocations, then handle main program dependencies, TLS, symbol resolution, and initialization. It is a special startup object; it should not be described as "needing another ld.so to run" just because it is ET_DYN.
Mapping does not mean every page is immediately read into physical memory. The first access may still trigger a page fault; page faults and I/O seen before and after main cannot be attributed solely to the source code stage. For specific loading implementations, refer to Linux v6.18 binfmt_elf.c.
exec preserves process identity, but not the old C program's context
Understanding exec as "calling another main" leads to misjudgments about resources and control flow. Heap objects, function pointers, and atexit registrations in the old address space are no longer part of the new program's execution state; the new program starts from its own startup path and does not return to the old main. Linux handles various attributes differently. execve attribute changes
State
After successful exec
Impact on program design
PID
Preserved
The same process identity can experience different program images
Old code, heap, stack, and mappings
Replaced
Old addresses cannot be passed as valid pointers for the new program
File descriptors
Usually preserved, except those with FD_CLOEXEC
Need to clarify which handles belong to the handover interface
Signal handlers
Disposition of caught signals reset to default
Old function addresses cannot continue to serve as handler entries
Signal mask
Preserved
The new program may inherit blocked signals
Other threads
Destroyed
exec does not make old threads run in parallel with the new main
Old atexit registrations
Not preserved, nor executed by exec
Cannot rely on them for business cleanup before exec
Below, we use execv to load the target program from the same experiment directory, inheriting the environment but specifying argv anew. Save as exec-launch.c; on success, you won't see "exec failed", but on failure, the old program can usually still report errors. Linux rarely has failures that cannot recover the old program after crossing the critical point of image replacement, so "failure always returns" should not be written as an absolute guarantee.
The PIDs on both lines should be the same, and the target receives argc=2, argv0=chosen-name, arg1=hello. argv[0] is set by the caller and does not reliably prove the actual path of the executable file. The launcher calls fflush first to ensure output in the old stdio buffer is written to the file descriptor; exec itself does not flush output for the old libc.
The initial stack is not a normal function call stack
_start is not entered from the caller of main via a normal C call; there is no upper-level function to which one can arbitrarily ret. Startup code should read the initial stack according to the target ABI and satisfy the stack alignment required for calling C functions.
Taking native x86-64 glibc as an example, _start prepares the arguments required by __libc_start_main:
Argument
System V AMD64 Location
Source and Purpose
main
rdi
Address of the application's main
argc
rsi
Number of arguments in the initial stack
argv
rdx
Pointer array of arguments in the initial stack
init
rcx
New startup code passes NULL; libc finds corresponding initialization info
fini
r8
New startup code passes NULL, not an "eighth internal flag argument"
rtld_fini
r9
Saves the cleanup function passed by the loader
stack_end
Passed on stack
Boundary information related to the initial stack
Therefore, simply copying pop argc and aligning rsp, while omitting the assembly to set the main address and rcx, cannot serve as a correct startup example. glibc's x86-64 start.S also includes CFI, PIC, and handling for different data models; directly viewing the target version is more reliable than mixing source code from several generations. glibc start.S
Initialization work is scattered between the loader and libc
Basic conditions for dynamic programs, such as TLS, need to be ready before user code depending on them is executed; not all initialization can be placed into a pseudo-C function in __libc_start_main. The initialization of the main program and dependent libraries is also not an unconditional "traverse all libraries, then call init pointers" process.
In the current glibc path, the loader handles corresponding dependencies and preinit work; libc startup code then handles DT_INIT/DT_INIT_ARRAY for the main program. Old startup objects might pass initialization functions, while the init parameter for new startup objects is NULL. Dependency cycles and constructor order between different objects cannot be casually assumed based on file names or link command surface order. glibc libc-start.c
.init/.fini usually contain instructions, while .init_array/.fini_array are arrays of function pointers. C++ non-local object initialization and compiler constructor attributes can enter the execution phase before main through these mechanisms, but constructor attributes and priorities are toolchain extensions, not unified rules of ISO C.
auxv: Auxiliary information passed from kernel to runtime
Key
Meaning of Value
AT_PHDR, AT_PHENT, AT_PHNUM
Runtime location, entry size, and number of the main program's program headers
AT_ENTRY
Main program entry address; even if the interpreter runs first, it still describes the main program
AT_BASE
Load base address of the ELF interpreter, cannot be treated as the main program base
AT_PAGESZ
System page size
AT_SYSINFO_EHDR
vDSO ELF header address if available
AT_RANDOM
Pointer to 16 bytes of random data, available for runtime initialization of protection values
AT_SECURE
Whether the loader should process environment etc. under secure execution conditions
AT_RANDOM is not a field controlling the ASLR switch. AT_SECURE is not triggered only by setuid. Reading the auxiliary vector should prioritize getauxval; inability to find it and a legitimate zero value can be distinguished by checking errno according to interface requirements. getauxval(3)
There is indeed an auxv after the initial environment pointer array, but environ during runtime may be migrated via operations like setenv, so one should not always continue scanning memory from the end of the current environ. Reading /proc/self/auxv or calling getauxval is more suitable for runtime queries.
Is the first instruction address the same as AT_ENTRY?
Look at "where the CPU jumps now" and "which object auxv describes" separately. Changing the interpreter base address changes the position of the first instruction, but does not change AT_ENTRY; this is exactly why breakpoints for starti and main fall in different stages in dynamic programs.
Preparing the visual
Is the first instruction address the same as AT_ENTRY?
When PT_INTERP exists, the interpreter runs first; AT_ENTRY is still the main program entry, and AT_BASE describes the interpreter's load base address. main is called later by user-space startup code.
{"id":"startup-entry-auxv","title":"Is the first instruction address the same as AT_ENTRY?","summary":"When PT_INTERP exists, the interpreter runs first; AT_ENTRY is still the main program entry, and AT_BASE describes the interpreter's load base address. main is called later by user-space startup code.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"interp\" checked><span data-i18n=\"interp\"></span></label><div class=\"control\"><label for=\"mainbias\" data-i18n=\"mainbias\"></label><output for=\"mainbias\" id=\"mainbias-value\"></output><input id=\"mainbias\" type=\"range\" min=\"0\" max=\"8\" value=\"2\" step=\"1\"></div><div class=\"control\"><label for=\"ldbias\" data-i18n=\"ldbias\"></label><output for=\"ldbias\" id=\"ldbias-value\"></output><input id=\"ldbias\" type=\"range\" min=\"16\" max=\"24\" value=\"16\" step=\"1\"></div><div class=\"metrics\"><div><span data-i18n=\"first\"></span><strong id=\"first\"></strong></div><div><span data-i18n=\"entry\"></span><strong id=\"entry\"></strong></div><div><span data-i18n=\"base\"></span><strong id=\"base\"></strong></div><div><span data-i18n=\"owner\"></span><strong id=\"owner\"></strong></div></div><p class=\"formula\" id=\"equation\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.metrics strong{font-size:17px}#equation{white-space:pre-line;min-height:6em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const yes=$('#interp').checked,B=+$('#mainbias').value*4096,L=+$('#ldbias').value*4096,h=n=>'0x'+n.toString(16);set('mainbias-value',h(B));set('ldbias-value',h(L));$('#ldbias').disabled=!yes;set('entry',h(B+0x1200));set('base',h(yes?L:0));set('first',h(yes?L+0x800:B+0x1200));set('owner',t(yes?'ld':'main'));set('equation','AT_ENTRY = B + 0x1200 = '+h(B+0x1200)+'\\nPC = '+(yes?'AT_BASE + 0x800':'AT_ENTRY')+'\\nmain ≠ e_entry');}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"base":"AT_BASE","entry":"AT_ENTRY","first":"First user instruction","heading":"Is the first instruction address the same as AT_ENTRY?","interp":"PT_INTERP exists","intro":"Choose whether there is an ELF interpreter, and adjust the load offsets of the two images respectively.","ld":"Interpreter entry","ldbias":"Interpreter load offset (pages)","main":"Main program _start","mainbias":"Main program load offset (pages)","owner":"Object running first","reset":"Reset","scope":"Abstract ELF layout, e_entry(main)=0x1200, e_entry(interpreter)=0x800, page size 4 KiB. No interpreter does not mean no libc initialization.","scopeLabel":"Model scope and conventions"}}
Observe before and after main, not just draw arrows
Below, we use GCC/Clang's constructor and destructor extensions to observe the execution order in a main program:
In this glibc example, the constructor runs before main; after main returns normally, the atexit callback and corresponding cleanup work are executed. This experiment does not prove that there is an arbitrarily dependent global initialization order between different shared libraries. The actual page size should be read from the output, do not hardcode it as 4096.
Normal return from main corresponds to libc's exit flow, which calls exit handlers and flushes stdio. _Exit/_exit skips these user-space cleanups, and process crashes or forced terminations do not guarantee they are completed. The original Linux exit system call and libc's process exit encapsulation also involve thread scope differences; see System Call ABI. exit(3)
Verify exit mode and output buffer separately
Save as exit-mode.c. Explicit full buffering ensures the experiment does not depend on whether stdout is connected to a terminal; exit handlers use standard error output for markers, facilitating separate checks from normal output.
Both exit statuses should be 7. On normal return, normal.out contains pending, and normal.err has atexit ran; on immediate exit, both files are empty. If the script enables set -e, it should explicitly accept the expected non-zero status, otherwise it may stop after the first run.
This shows that exit codes and user-space cleanup are two independent pieces of information: the same status code does not prove that the same cleanup actions were executed. Normal exit flushing stdio does not equal data persisted to storage devices; programs requiring persistence semantics must arrange synchronization according to file system and interface conventions. exit(3), _exit(2)
Static linking still has startup work
Build Form
Common Entry Arrangement
Still Needs Attention
Dynamic PIE
PT_INTERP, interpreter runs first
Dependency mapping, relocations, initialization
Static non-PIE
Usually no PT_INTERP, direct main entry
libc startup, TLS, constructors, possible IFUNC
Static PIE
Usually no PT_INTERP, direct main entry
Self-relocation and runtime initialization
Custom libc-free program
Custom entry
Comply with ABI and exit protocol yourself
Static linking usually extracts needed members from archives, not unconditionally stuffing the entire libc into the file. Static programs may still contain GOT/PLT-related structures or require IFUNC relocations; static PIE explicitly needs to handle position changes. "Static means no relocations, startup is definitely ten times faster" cannot hold true; comparisons should be made under the same functionality and environment.
How to locate crashes before main
First, use readelf to confirm the main entry, interpreter, and dependencies, then use starti in gdb ./startup to stop at the first user-space instruction; dynamic programs may be in the interpreter at this point. You can then set breakpoints at main, constructors, or __libc_start_main. Whether internal symbols are visible depends on debug symbols and build methods.
strace -e trace=execve,openat,mmap,mprotect ./startup can display system calls during the loading phase, but not all user-space initialization. If dependencies are successfully mapped but main has not been reached, continue checking relocations, IFUNC, constructors, and TLS usage, rather than just staring at the first line of main.