-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wrapping calls in the same compilation unit #2
Comments
Hi Matthijs, First of all, about weak symbols: while I didn't experiment right now, but I really doubt that they will ever work for calls in the same translation unit. Actually, anything which would rely on Although, I might have tried before but I did not experiment with them right now before answering. So, some experiments will tell the correct answer. Another issue with weak symbols is that you cannot call original function. And, they are not good for third party libraries. And, one of the design goals I had was to not require compiling separately for tests: So, to me also option 3 seems the best of all, although I'm still unsure if it'll work specially when optimizations are on (till now, I've tried to not force the user to compile with different settings, like not having optimizations. Although, LTO is currently not supported. Unfortunately, interactions are not as simple as it seems in compiling/linking process. For example, you see that bind_fakes supports two methods of renaming symbols: 1. direct method using objcopy 2. a logical method using (Unfortunately, I've not written when it didn't work :( ) |
Just tried this method, and it simply stops working if you add Also, the other answer (option 1) also didn't work with |
Yeah, take your time. For a large part, I'm writing all this down to structure my own thoughts and remember them for later, so don't feel pressured by my tendency to write (too) much :-p
That's not what I've seen in my tests and review of the LD internals. E.g. consider:
Note that even though the call to This also means that you can make the entry weak after compilation and replace it with some other function.
This is true, you'd probably need to run with
This is indeed a problem, that can be worked around by adding another name for the original function. This is what option 2 above suggests.
What do you mean by that? That it requires modifying the source code to make declarations weak? Seems this is not strictly required, as suggested in option 1 above, you can make things weak aftewards using
Yeah, I think this is also a relevant quality (though requiring
Yup, that causes the call to be inlined, and when that happens, you're done. Using I wonder if maybe LTO could actually be used to our advantage here: If you can postpone all inlining until link time, and inlining only happens after the |
Yeah, you are correct: adding The downside is that it needs even more work to be done during build process, and also it seems to not be able to replace the About LTO: unfortunately, the tools are not mature enough and well integrated, at least the last time I checked: the Well, I found related bugs: |
All in all, I also think I prefer solution 3 too! |
Ah, that would completely block that path, it doesn't seem like this is easy to fix in gcc / ld either. But not supporting LTO is ok, especially since you can even enable LTO during compilation as long as you use fat objects (containing both partially compiled code for LTO and fully compiled code for normal linking) and then use LTO during the regular link and non-LTO for the test link.
Cool! I'll probably see if I can hack something up for this, but not right now: I really need to get back to some actual progress on the underlying project, rather than goldplating PowerFake to maybe do unit testing on this project in the future :-p |
Good. About LTO, I'm not sure if that is possible. Seems that handling even fat LTO files is special. IIRC, I've tried using a fat-LTO main library with non-LTO test executable with no success. But I'm not sure. |
I previously tried applying option 3 manually on Linux, using ELF object files, which seemed to work as expected. Today, I tried the same on MinGW64, which uses COFF object files, but that did not work right away. Maybe some additional changes are needed, or some other approach is needed there. I might dive into the ld sources later to see what it does for COFF (I think MINGW64 just uses gcc / ld compiled for Windows). What I did to do this manually is to use |
Looking more closely at the output of |
Thanks for the research! It'd be great if we can solve it for mingw too, but if it becomes a linux only feature it'll still be great. |
Well, I'm about to merge a new macro: HIDE_FUNCTION. It is a very lightweight macro: at the end, it uses a link script which works similar to usign ld's --defsym: so it hides the original function and doesn't provide any means to call the original function. Additionally, as you mentioned already, it doesn't work with mingw. Also, in addition to function inlining, Despite all these limitations, if all those are acceptable, it lets you to capture calls inside the same TU. It is far from ideal, but still simple enough and doesn't need a special tool to manipulate object files. Although I'll keep looking for something better. Update: Merged with 96c7da9 |
Have you tried the weak attribute? In this way you mock a function without moving it to other file |
To be honest, I don't remember now. However, I wonder if it provides any benefits over the current |
weak attribute is standard gcc, similar to ld wrap, is possible to create a macro and build twice, with and without weak attribute, when weak is enabled you can mock the original function, when weak is disabled you can call the real function |
Well, |
I don't think I understand very well how EXTERN/undefined ld option works in HIDE_FUNCTION. According to this link seems to work only for libraries and not for functions from same object files |
Well, no.
However, unlike The way it works is that it marks the symbol as undefined, forcing the linker to look for the symbol in other object files, so that it links our fake function instead of the original. |
So if you want to mock a function (from same unit) but also call the original you need to have two builds, right? |
The flag only affects the test executable; so in the main binary you are fine. But in tests, yeah if you need some tests to be able to call the original function, yeah you'd need a separate test binary. (the original object file doesn't need to built twice, but you need two links: a test binary linked to the faked function, another one to link to the original function. AFAIK, you need to do the same with weak symbols too. |
I wrote a tool because I was observing the same issue, that with --wrap you cannot wrap symbols in the same compilation unit. Here the link: PS: Only ELF is supported and it is not very extensively tested yet, so problems may occur! |
Thanks for the link! I've already provided Although, I've tried to not get involved in very low level modifications, it can be provided as an option. |
would it be possible to modify the script so that functions from a CU be wrapped and be possible to call them from outside CU file? |
starting from @wafgo python LIEF interface I have copied functions (only those for which I have created a wrapper) from compilation units to _orig, then I made the functions undefined in ELF object. In this way seems to work also with LD wrap. Wrap functions are still accessed via _wrap prefix but original functions are accessed via _orig prefix. I hope I did not missed an important detail :) |
Thanks for the report! I'll try to have a look to both WrapMaster and LIEF itself, and may provide using it as an option (might port WrapMaster functionality to C++ and using LIEF directly). |
A limitation of the
--wrap
method, is that it cannot intercept calls to a function inside the same compilation unit as where the original function is defined. This can be solved by moving to-be-intercepted functions into their own .cpp files, but that hurts the readability of the original code, requires changes to the original code when adding wrapped functions in the testsuite, and makes it a bit more fragile to breaking the wrapping when the original code is changed. So I'd like to see this limitation lifted. I've been digging into theld
sources to find a way around this, and this issue presents a couple of options for this. Below is a more generic writeup for handling wrapping, not limited to the PowerFake usage necessarily.How ld does linking
In a gross oversimplification of the tremendously complex linking process, here's what
ld
does when it links an executable. I've mostly looked at linking elf object files into elf executables (specificallyelf64-x86-64
), but I think most of this will be similar for other targets as well (and the elf target is what we're using in almost all cases anyway).Resolving symbols
elf_x86_64_relocate_section()
) and entered into a global (in-memory) symbol table (that starts out empty). Note that the symbol table also contains undefined symbol entries, for symbols that are referenced but not defined._bfd_elf_merge_symbol()
). If the name is not in the global table yet, it is simply added. If it is already there, the existing symbol and the new symbol are merged (_bfd_elf_merge_symbol()
and_bfd_generic_link_add_one_symbol()
). This merging handles this like a strong symbol replacing a weak symbol (or a weak symbol being discarded when there is already a strong symbol), or a strong symbol replacing a previously undefined symbol, raising an error when trying to merge two strong symbols, etc.UNDEF foo()
entry first (from a file that references but does not definefoo()
). This creates a newUNDEF foo()
entry in the global symbol table, which is associated with the entry in the current file's symbol table. Then, in another file that actually definesfoo()
, aDEF foo()
entry is processed. This looks in global table, finds the existingUNDEF
entry, and merges it with the newDEF
entry by overwriting the existingUNDEF
entry with the newDEF
entry. This also causes the (UNDEF
) entry in the first file, to be associated with this new, merged,DEF
symbol, so it can be found later.Resolving relocations
call 0x0
. It then also leaves an instruction for the linker, saying "Put the address of the global symbolfoo
at this address (i.e. after thecall
)". This instruction is called a relocation.foo
" is not so explicit. Instead, a relocation refers to an entry in the file's symbol table.foo()
itself, the symbol table contains anUNDEF foo()
entry. After resolving symbols, as described above, the relocation is resolved by looking at the associated file symbol table entry, which has an associated global symbol table entry, which is now aDEF foo()
entry that tells the linker wherefoo()
is actually defined.foo()
itself, the symbol table contains aDEF foo()
entry. In most cases, the associated global symbol table entry will be this sameDEF foo()
entry, so the linker resolves relocations forfoo()
to the definition in this file itself. There can be exceptions, e.g. whenfoo()
is weakly defined, then the actualfoo()
to be used by the relocation can still be in a different file.Implementing
--wrap
To implement the
--wrap
option, the linker changes the second step in the resolving symbols procedure. When it "looks up any existing symbol by the same name in the global symbol table", and the name to look up was passed to--wrap
, it actually does a lookup for__wrap_<name>
instead. Similarly, when it has to look up__real_<name>
, it looks up<name>
instead. Simple, but quite powerful.Except that it can only do this for
UNDEF
entries in the file's symbol table. Consider what would happen otherwise: There is aDEF foo()
entry in one file. The global table lookup uses__wrap_foo
instead, and finds an existing entryDEF __wrap_foo()
, which is the wrapper to be used. Trying to merge these two entries will fail, since both are strong definitions. If you would instead discard theDEF foo()
(as if it were weak) and associate theDEF __wrap_foo()
entry with the file symbol table entry, then relocations (calls tofoo()
) in the same compilation unit would correctly resolve to__wrap_foo()
. However, you would have discarded the originalfoo()
entry, so you can no longer access it through__real__foo()
.I guess the linker could have also (in addition to looking up
__wrap_foo()
and associating the result with the current entry) put the originalDEF foo()
entry in the global symbol table, without associating that entry with any symbol table entry in the current file (but putting it out there to be associated with other entries in other files later), but maybe this didn't seem relevant, or maybe this has a ton of unexpected side effects (the linker is horrendously complex after all, I'm just showing the supersimplified version of it here).Diverting local calls
This analysis does suggest two possible ways you can still divert a call to a locally defined function to some other function:
--wrap
)UNDEF foo()
symbol that can be wrapped, and have a secondDEF foo()
symbol with the actual definition. This is essentially what you do when you move the definition offoo()
into its own source file, but I think this could be done inside a single.o
file as well.In addition, Greg Carter shows that you can also use e.g.
-Wl,--defsym,foo=__wrap_foo
as a linker option to forcibly replace thefoo
function with the wrapper. I haven't full investigated how this works in the linker internals, but I believe that this approach loses access to the original symbol (unless you duplicate it under another name by modifying the object files, as suggested below), so I haven't investigated this option much further.How to wrap local calls
So, how can you then actually achieve
--wrap
for local calls? The above suggests some ingredients, below I'll mix those into a couple of different (but similar) approaches.A downside of all below approaches is that they require inspecting and modifying the object files in the build. A solution where you would not need to inspect the object files at all (or maybe just the object files that contain the wrappers, to get a list of wrapped functions) and handle everything by just adding compiler and/or linker flags would be ideal, but I haven't been able to figure out a way to allow this.
1. Weakening symbols, without
--wrap
objcopy --weaken-symbol
to do so after compilation (to prevent having to modify the original source files).--wrap
anymore.--globalize-symbol
to ensure the symbol is global, but I think that's only needed for non-exported (i.e. globals with thestatic
keyword) symbols, and making those global might end up creating conflicts that were not previously present, so this must be done with care).2. Weakening symbols and adding a
__real_
version, without--wrap
objcopy --weaken-symbol
to mark the original as weakobjcopy --add-symbol
to add a new symbol called__real_<name>
with the address (i.e. pointing to the same bit of compiled code). This new symbol should ideally be a perfect copy (same section, size, visibility, flags, weakness, etc.), and the copy should be made in all compilation units that have the function (except where the wrapper is defined), so that if the existing symbol already has multiple copies (e.g. a weak and strong version), it will still resolve as without these changes.__real_<name>
from the wrapper to access the original.objcopy
can create a proper identical copy, so might require building a custom command or script to parse and modify the elf file.3. Splitting symbols, with
--wrap
UNDEF
instead. This result in two symbols by the same name, where relocations point to the UNDEF one and the DEF one points to the implementation, allowing to use--wrap
as normal.objcopy
can do this, so this probably requires building a custom command or script to parse and modify the elf file.4. Reimplementing
--wrap
__real_<name>
and replace the original entry with anUNDEF __wrap_<name>
(so that existing relocations now point to the wrapper).UNDEF __wrap_<name>
entry.objcopy
can do this, so this probably requires building a custom command or script to parse and modify the elf file.--wrap
implementation.I'm inclined to further investigate the last option (if we need to modify .o files with custom tooling anyway, might as well do the entire wrap thing ourselves, which might also simplify things because we no longer need to comply with the linker's requirements on
__wrap_
naming). However, the fact that these files are no longer usable as part of a regular build is a bit annoying, and might make option 3 more suitable (if it works, I haven't tried it yetA quick manual edit using https://elfy.io suggests this indeed works). Option 1 is not feasible, since it does not allow calling the original function, and option 2 feels a bit fragile when it comes to existing weak functions.The text was updated successfully, but these errors were encountered: