3 ms·
Why should every function start with endbr64 command? Aren't functions usually called directly? Also, is it required to insert endbr64 command after function c
by codedokode 3y ago
Why should every function start with endbr64 command? Aren't functions usually called directly?
Also, is it required to insert endbr64 command after function calls (for return address)?
- messe 3y agoC allows for any function to be called via a function pointer, and functions can be in different translation units, so the compiler can't simply assume that a function will never be called indirectly and has to pessimistically insert endbr64 in order to maintain a reasonable ABI. And no, as I understand it, this is only for branch/calls not returns.
- Joker_vD 3y agoWell, if the function is marked "static", the compiler can actually check whether the function's address is taken in the current compilation unit or not and omit/emit ENDBR64 accordingly (passing pointers to static functions to code in another compilation units is legal, and should still work).
- messe 3y agoGood catch. Yeah, as long as the functions address is never taken the compiler has a lot of leeway with static functions; it can even avoid emitting code for them entirely if it can prove they're never called or if it's able to compute their results at compile-time.
- josephg 3y agoYep. Or inline them at every call site if that makes sense to do based on the optimization level and flags.
- MobiusHorizons 3y agoIs this theoretically something lto could remove?
- tedunangst 3y agoIf you disable dlopen and ld_preload.
- codedokode 3y agoDlopen() "sees" only functions marked as exported (with macro like DLLEXPORT on Windows), not every function or am I wrong? Is C that bad?
- tedunangst 3y agoOn openbsd at least, every global symbol is exported unless you use an explicit symbol list. It's unusual for executables.
- aidenn0 3y agoA traditional compiler needs to insert them for all external functions, because other compilation units may make an indirect call.
- josephcsible 3y ago> Why should every function start with endbr64 command? Aren't functions usually called directly? They're usually called directly, but unless the compiler can prove that they always are (e.g., if they're static and nothing in the same file takes the address), endbr64 is required. > Also, is it required to insert endbr64 command after function calls (for return address)? No, IBT is only for jmp and call. SS is the equivalent mechanism for ret.
- derefr 3y ago> but unless the compiler can prove that they always are (e.g., if they're static and nothing in the same file takes the address), endbr64 is required Then why not just have the compiler break down every non-static function into two blocks: a static function that contains all the logic, and a non-static function that just contains an IBT and a direct jump to the static function? (Or, better yet, place the non-static label just before the static one, and have the non-static fall through into the body of the static.) Then the static direct callsites won't have to pay the overhead of executing the IBT NOP.
- deleted 3y ago[deleted]
- tedunangst 3y agoWhat is the overhead of executing the IBT NOP?
- 95014_refugee 3y agoIt's not "executed" per se. It consumes space in the cache hierarchy, and a slot in the front-end decoder. It won't ever be issued, but depending on the microarchitecture in question it might result in an issue cycle having less occupancy than it might have had in the case where the subsequent instruction was available. With that said, the first few instructions of a called function often stall due to stack pointer dependencies, etc. so the true execution cost is likely to be even smaller than the above might suggest.
- 3y ago
- eklitzke 3y agoAs to why they're not always called directly, imagine some code like this: int FooWithoutChecks(void *p); int Foo(void *p) { if (p == NULL) return -1; return FooWithoutChecks(p); } In general the caller is expected to call Foo if they aren't sure if the pointer is nullable, or if they already know that pointer is not null (e.g. because they already checked it themselves) they can call FooWithoutChecks and avoid a null check that they know will never be true. The naive way to emit assembly for this is to actually emit two separate functions, and have Foo call FooWithoutChecks the usual way. But notice that the FooWithoutChecks function call is a tail call, so the compiler can use tail call optimization. To do this it would inline FooWithoutChecks into Foo itself, so the compiler just emits code for Foo with the logic in FoowithoutChecks inlined into Foo. This is nice because now when you call Foo, you avoid a call/ret instruction, so you save two instructions on every call to Foo. But what if someone calls FooWithoutChecks? Simple, you just call at the offset into Foo just past the pointer comparison. This actually just works because Foo already has a ret instruction, so the call to FooWithoutChecks will just reuse the existing ret. This optimization also saves some space in the binary which has various benefits in and of itself. The example here with the null pointer check is kind of contrived, but this kind of pattern happens a LOT in real code when you have a small wrapper function that does a tail call to another function, and isn't specific to pointer checks.