Ghidra: finding main when the entry point is startup
The entry point of a hosted C program is startup code. main is a function that startup calls. You find it by following that handoff, and you confirm it by the strings and calls that belong to the program you recognize.
The loader starts at entry
ELF and PE record an entry address. Ghidra labels it entry. On a C program built for a normal operating system, that address is the runtime’s start code, not the main you wrote. The runtime’s job is to set up the stack, find argc and argv, run constructors and other initialization, call your main, and then end the process with the status main returned. The function is short in the decompiler and noisy in the Listing, full of small moves that prepare arguments.
If the binary still has symbols, main may already be a name in the Symbol Tree, and _start may already be the entry. Use the name, then still open the body and confirm the config string is there. Symbols are evidence. They are also occasionally stale in odd builds. The confirmation takes one jump and removes the doubt.
The glibc handoff
On a typical dynamically linked glibc program, startup calls __libc_start_main. One of the arguments is a pointer to main. In the decompiler this often appears as the first argument: a function name or a raw address passed into the call. Go to that address. The function you land on should look like your program. In this lab it chooses a path, calls the loader, and prints a status. If the function you landed on is still shuffling registers and calling into libc internals, you followed an argument that points at a constructor or at the runtime itself. Go back and take the pointer that leads to your string.
The exact register dance inside _start changes between glibc versions and between compilers. Do not memorize a byte pattern for it. Memorize the shape: a short startup function, a call to the runtime helper, a function-pointer argument, and a body you can recognize. musl and other libcs use a different helper name and the same idea. The string confirmation still works when the helper’s name is unfamiliar.
Windows, libraries, and images with no main
A Windows executable starts at a CRT entry that eventually calls main, wmain, WinMain, or wWinMain, depending on the subsystem and the character width. The practical method is the same. Follow calls out of entry through the short CRT functions until you reach a function that references your strings or your window logic, and whose parameters look like a program’s arguments rather than like another layer of setup. Ghidra’s PE analyzers sometimes name that function for you. Verify the name by reading the body.
A shared library, a driver, or a raw firmware image does not owe you a main. Exports, interrupt vectors, or a reset handler are the starts those files actually have. Forcing the glibc story onto them produces a fictional main and a confident write-up about a function that never runs. The memory-map lesson’s question is the fallback. What did the container, or the lab notes, mark as the start, and what references does that address have?
Lab: name main from evidence
Make a stripped copy if your current program still has the symbol main: copy the binary and run strip on the copy, then import the copy under a new name so the unstripped program stays in the project. Open entry. Find the call to __libc_start_main or to whatever runtime helper your toolchain emitted. Open the function-pointer argument that leads to the config path. Name that function main only after the string reference is on the screen, and put the evidence in the plate comment.
If analysis had already named main, do not skip the check. Open it, confirm the string, and write “symbol agreed with the config-path reference” in the notebook. The skill is the confirmation. A name by itself is a claim.
Plate comment you should be able to defend: main: function pointer passed to __libc_start_main. Body references "/etc/harbor/check.conf" and calls load_config. Named after both facts were on screen.
Diagram
From the file’s entry to the function you wrote
- entry
The container’s entry symbol is where the loader starts. On a normal C program this is startup, often labeled entry, and on glibc it is the body of _start.
- Runtime
Startup prepares arguments, runs constructors, and calls a runtime function. On a typical glibc link that function is __libc_start_main.
- Pointer
One argument of that call is a function pointer. On a hosted glibc program, that pointer is main. Go to it.
- Confirm
The function you landed on should reference something only your program would reference, such as the default config path. If it does not, you followed the wrong pointer.
Entry label and address: Runtime call the entry reaches: __libc_start_main (glibc example) Argument that is a function pointer: <address> Landed in a function that references: /etc/harbor/check.conf Conclusion: this function is main, confirmed by that string.
Entry label and address: Runtime call the entry reaches: __libc_start_main (glibc example) Argument that is a function pointer: <address> Landed in a function that references: /etc/harbor/check.conf Conclusion: this function is main, confirmed by that string.
Check yourself
1. You open a stripped harbor-check and the Symbol Tree’s entry function is a short routine that calls __libc_start_main. What is entry?
2. The decompiler of entry shows __libc_start_main(FUN_00101240, ...). You open FUN_00101240 and it references /etc/harbor/check.conf. What do you name it?
3. The file is a shared library. It has exports, and it has no call to __libc_start_main. Where do you start instead?