This is an archive of the discontinued LLVM Phabricator instance.

Add .init_array support to Wasm LLD
AbandonedPublic

Authored by ncw on Nov 29 2017, 10:29 AM.

Details

Reviewers
sbc100
modocache
Summary

I hope this is the right approach!

What I've done is added some (hacky?) plumbing so that for Wasm we export the start/end of .init_array and .fini_array using the well-known __{init,fini}_array_{start,end} symbols.

(Aside: the way this is implemented on desktop (Linux) platforms is that the driver which invokes LLD adds some special files "crtbegin.o" and "crtend.o" to the link line. Those are shipped as part of the platform, and register dummy functions with the magic names __{init,fini}_array_{start,end}, and in turn libc has a weak dependency on those symbols so that it can traverse the array of symbols.)

I had a go with the same approach as on desktop Linux - but it didn't work quite as simply as I'd like, plus there's the nuisance of actually shipping and maintaining the crtbegin/end.o files...

The approach I've gone for instead is to explicitly fudge in those symbols in LLD, and export their addresses manually.

It all works - which is perhaps the only redeeming virtue of this sort of special-casing.

Note this approach should be portable to other libc implementations (not just Musl), since the symbols __init_array_start etc are mandated by the ABI (Musl doesn't create/define those, it relies on GCC to supply them). So, creating those symbols ourselves in LLD is OK, we can add a note to the C ABI document on GitHub to say that crt1.o and __init_array_start from Musl's point of view will work exactly as on desktop linux.

Testing

I've tested with Musl, and the tests in lld/tests/wasm showcase a basic C++ file that uses a global static constructor and a C function annotated with __attribute__((constructor)). The Wasm file at the end does indeed run nicely in the browser, calling each global constructor exactly once.

Diff Detail

Repository
rLLD LLVM Linker

Event Timeline

ncw created this revision.Nov 29 2017, 10:29 AM

I'm not too bothered by the special-casing here.

That said, I have an idea for a different approach and I'm curious what you and others think. Instead of traditional .init_array sections, what if we taught the linker to link multiple start sections, by synthesizing a new start function that calls all the start functions from the input? So if you link a.o and b.o, and each have a start function, the output would contain a start function containing two calls, one to a.o's start function and one to b.o's. Start functions never have arguments or return values, so the synthesized start function would just look like "0x10 <first callee> 0x10 <second callee> ..." (0x10 is the call opcode).

This would require changes to how eg. musl and other things work. However, it would fit well within the spirit of using the wasm format for .o files in a natural way.

(There's also .fini_array, but we can talk about that if the overall idea is worth pursuing.)

ruiu added a subscriber: ruiu.Nov 29 2017, 2:26 PM

I wonder if you can do everything in the loader. We can let lld create .init_array and .fini_array (which is default behavior) and make a change to the loader so that the loader executes each function pointer in .init_array and .fini_array sections, no?

ncw added a comment.Nov 30 2017, 12:55 AM

@sunfish, I think that's an interesting idea. Here are some thoughts I've had:

Pros of building "composite start function" on link:

  • Uses native WebAssembly information rather than custom section. That is a genuinely nice change.

Cons:

  • Adds asymmetry to destructor handling, when it feels like they should be mirror features
  • Won't work out of the box with musl (needless friction with upstream?)
  • Less extensible to other sections. I don't expect any other sections to need this treatment, currently, but maybe other languages apart from C have initialisation concerns - like Haskell or Rust...? Sticking as close to ELF as possible for C sets a good precedent for making those other languages easy to integrate. If you're suggesting adapting Musl's startup to WebAssembly, are we going to have to adapt the runtimes of other languages too?
  • Assumes that start functions can be run in any order, but that's actually not exactly the case. In particular, libc does its own initialisation _before_ any user-defined constructors, so that thread-local storage and printf are usable in all user-supplied code. It's a bit cheeky of libc to get special privileges like that (by virtue of supplying the entrypoint, it's able to do its own initialisation first). There would have to be a way to force this behaviour to continue - but it wouldn't be insurmountable, you'd just to have some "loose" object file (eg. "crt2.o") that we could guarantee appears first on the link-line.
In D40614#939712, @ruiu wrote:

I wonder if you can do everything in the loader. We can let lld create .init_array and .fini_array (which is default behavior) and make a change to the loader so that the loader executes each function pointer in .init_array and .fini_array sections, no?

I'm not sure what you mean by the "loader" - is that the runtime WebAssembly interpreter/AOT-compiler? At runtime, the "sections" don't exist anymore, they're only available here to the linker. At runtime, you're right we want the interpreter to execute each function, and that's only achievable in WebAssembly via the START function.

Cons:
Adds asymmetry to destructor handling, when it feels like they should be mirror features

Actually, it's not entirely clear that .fini_array is desirable for current wasm. It's nice to be compatible with tools that expect it, but it's usually better to use atexit/__cxa_atexit instead, because that makes it feasible to run destructors in reverse order of the runtime order of the corresponding constructors. For example, C++ uses __cxa_atexit and not .fini_array on ELF, in part for this reason.

Won't work out of the box with musl (needless friction with upstream?)

It would be some friction. I don't think it's needless though :-).

Less extensible to other sections. I don't expect any other sections to need this treatment, currently, but maybe other languages apart from C have initialisation concerns - like Haskell or Rust...? Sticking as close to ELF as possible for C sets a good precedent for making those other languages easy to integrate. If you're suggesting adapting Musl's startup to WebAssembly, are we going to have to adapt the runtimes of other languages too?

ELF is a standard that some tools support, so by emulating ELF, we make it easier to port these tools.

On the other hand, there are people looking to port and build tools for wasm that have no knowledge of ELF. If we do things the wasm way, rather than the ELF way, it'll be easier to promote interoperability between all these tools on wasm, because wasm is the thing they're all guaranteed to have in common.

Assumes that start functions can be run in any order, but that's actually not exactly the case. In particular, libc does its own initialisation _before_ any user-defined constructors, so that thread-local storage and printf are usable in all user-supplied code. It's a bit cheeky of libc to get special privileges like that (by virtue of supplying the entrypoint, it's able to do its own initialisation first). There would have to be a way to force this behaviour to continue - but it wouldn't be insurmountable, you'd just to have some "loose" object file (eg. "crt2.o") that we could guarantee appears first on the link-line.

We'd just do the same thing that ELF does: order the functions in the combined start function in the order they appear on the link command line. Then we just put the initialization in crt1.o/crti.o/crtbegin.o and but those before any user code, and we're good, just like ELF.

ncw updated this revision to Diff 125113.Dec 1 2017, 5:28 AM

(Rebased.)

We probably need some move votes then to reach consensus. On the one hand, Dan's idea is maybe a bit cleaner for C/C++ in the long term. There should probably be a principle on how much we want to emulate ELF, which we would then stick to. Choosing not to implement support for .fini_array (__attribute__((destructor)) on exit()) also seems a bit premature, although I admit it's probably hardly-used. Potentially a good number of Emscripten programs do call exit() though.

At the moment, LLD is far, far from being able to link "generic" Wasm files together. The entire model revolves around custom sections providing ELFy data defined in the "Tool conventions", so using a start section instead of .init_array really isn't going to a huge difference for generic tools that want to produce inputs for LLD to process. I can appreciate the desire to reduce the ELFyness of the conventions though.

Either way, we could still do this (smaller) change right now, and come back and do Dan's change later, once we've got C++ working.

ncw updated this revision to Diff 125120.Dec 1 2017, 6:02 AM

Rubbish, when I rebased I must have sent the wrong diff... OK, here's the correct rebased diff.

sbc100 edited edge metadata.Dec 1 2017, 5:22 PM

I've also been working on a similar change: https://reviews.llvm.org/D40760

Sorry its been in the works for a while but I hadn't uploaded it yet.

ncw updated this revision to Diff 125736.Dec 6 2017, 8:32 AM

CHANGES:

  • Rebased...
  • Integrated Sam's changes from D40760, in particular his tests and his renaming/refactoring (where possible)

I suspect there'll be some more conflicts as soon as D40859 is merged. I'm quite happy for D40859 to be merged first, and I can rebase this patch.

(NB. My entrypoint changes are "Ready to Land" in Phabricator, so I've rebased on top of those, since surely they'll be merged sooner than this. I'm just trying to minimise the amount of rebasing and conflicts.)

ncw updated this revision to Diff 126124.Dec 8 2017, 4:29 AM

As expected, it needed rebasing after D40859. I don't know whether this will be merged - but I've done the rebase anyway just so we're at least looking at the right change.

Now that Sam's Global changes are merged, it's a shorter diff.

ncw abandoned this revision.Dec 19 2017, 3:26 AM

Abandoning - but! I need to remember to keep the test files, which might still be useful.