Skip to content
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

llext: add support for init arrays - revised #77641

Merged
merged 3 commits into from
Sep 6, 2024

Conversation

pillo79
Copy link
Collaborator

@pillo79 pillo79 commented Aug 27, 2024

This replaces #76724, and includes all feedback received there.

Add support for the .preinit_array, .init_array and .fini_array sections in ELF files. These sections are arrays of function pointers that are filled by the compiler with the addresses of functions, such as C++ constructors, that need to be called at app startup or cleanup by the loader.

This is now achieved separately from llext_load, in whatever execution context is deemed secure by the application writer, by either calling directly llext_bringup/llext_teardown or starting a new user or kernel thread with the llext_bootstrap function. The list of function pointers is obtained in all cases via a syscall. (link to related doc)

The PR also adds a init_fini test that creates an ELF file with the required sections and verifies the execution order is correct. EDIT: Moved to #77941.

Supporting the .fini_array section requires more changes and is not currently implemented.

@pillo79 pillo79 force-pushed the pr-init-arrays-v3 branch 2 times, most recently from bd0f582 to d3d9141 Compare August 28, 2024 07:26
@pillo79 pillo79 marked this pull request as ready for review August 28, 2024 09:33
@zephyrbot zephyrbot added the area: llext Linkable Loadable Extensions label Aug 28, 2024
@pillo79 pillo79 requested a review from ceolin August 28, 2024 09:36
Copy link
Collaborator

@mathieuchopstm mathieuchopstm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall.

A few thoughts and some minor change suggestions/requests:

}

/* Start extension main function, if provided */
if (entry_fn) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it could be done by the caller, unclear what benefit passing entry_fn in and calling it here does.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning was to allow llext_bootstrap to be called directly by k_thread_create, to minimize boilerplate code. See the changes in test_llext_simple.c and the above linked doc.

Copy link
Collaborator

@teburd teburd Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh that makes sense, it’s user mode friendly then I take it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely. The LLEXT test suite in this PR already runs llext_bootstrap both in user and kernel mode 🙂

@pillo79 pillo79 force-pushed the pr-init-arrays-v3 branch from d3d9141 to 9623405 Compare August 29, 2024 10:11
@pillo79
Copy link
Collaborator Author

pillo79 commented Aug 29, 2024

v2:

  • addressed all change suggestions
  • added sanity check for returned pointers in llext_get_init_array

mathieuchopstm
mathieuchopstm previously approved these changes Aug 29, 2024
typedef void (*elf_init_fn_t)(void);

int init_fn_count = ret / sizeof(elf_init_fn_t);
elf_init_fn_t init_fns[init_fn_count];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check ret for a maximum value before allocating an arbitrary size array on stack?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The syscall returns sizes of arrays already in memory, so this number is "sane", but of course the size of the stack is unknown. Not sure how I could add overflow checks - suggestions?


if (buf) {
char *byte_ptr = buf;
const char **fn_ptrs = buf;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const void **? I had to think why char, thought there would be some particular purpose in it. Same for text_start and test_end below. In fact personally I prefer uint8_t * over char * in such cases for the 2 reasons: (1) these aren't characters or strings, and (2) uint8_t has a well defined signedness unlike char, but I do realise, that char is used a lot in such cases...

Copy link
Collaborator Author

@pillo79 pillo79 Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am only comparing the pointer values in a sanity check here, so the different types have no actual effect; choosing a byte-sized type allows to easily calculate text_end and compare pointers without ugly extra casts, and char * is used by memcpy() a few lines above.

};
static const void *const init_fn_ptrs[] __used Z_GENERIC_SECTION(".init_array") = {
init_fn
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we actually use C++ tests? Might be even nicer to actually have a couple of constructors as a test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... that would require proper C++ support for LLEXT (cmake, toolchains, etc). That's a different league - definitely not going that far in one PR. This is portable and has the same effect.

C++ tests will come in due time! We as Arduino kinda depend on that 🙂

Copy link
Collaborator Author

@pillo79 pillo79 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Will post an update soon.

typedef void (*elf_init_fn_t)(void);

int init_fn_count = ret / sizeof(elf_init_fn_t);
elf_init_fn_t init_fns[init_fn_count];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The syscall returns sizes of arrays already in memory, so this number is "sane", but of course the size of the stack is unknown. Not sure how I could add overflow checks - suggestions?


if (buf) {
char *byte_ptr = buf;
const char **fn_ptrs = buf;
Copy link
Collaborator Author

@pillo79 pillo79 Aug 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am only comparing the pointer values in a sanity check here, so the different types have no actual effect; choosing a byte-sized type allows to easily calculate text_end and compare pointers without ugly extra casts, and char * is used by memcpy() a few lines above.

};
static const void *const init_fn_ptrs[] __used Z_GENERIC_SECTION(".init_array") = {
init_fn
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... that would require proper C++ support for LLEXT (cmake, toolchains, etc). That's a different league - definitely not going that far in one PR. This is portable and has the same effect.

C++ tests will come in due time! We as Arduino kinda depend on that 🙂

Load the .preinit_array, .init_array and .fini_array sections in ELF
files. These sections are arrays of function pointers that are filled by
the compiler with the addresses of functions that need to be called at
startup or termination by the loader, such as C++ constructors and
destructors.

Signed-off-by: Luca Burelli <[email protected]>
llext_bringup() and llext_teardown() are intended to be used to call the
extension's own initialization and cleanup functions, respectively. They
are meant to be called by the developer after loading an extension and
before unloading it. The list of function pointers to be called is
obtained via the new llext_get_fn_table() syscall, so that they are
compatible with user mode.

llext_bootstrap() is intended to be used as the entry point for a thread
created to run an extension, in either user or kernel contexts. It will
call the extension's own initialization functions and then an additional
entry point in the same context (if desired). The same function can also
be called directly in the main thread, if only initialization is
required.

Signed-off-by: Luca Burelli <[email protected]>
Add documentation for the new LLEXT APIs that allow to call the
initialization and cleanup functions of an extension.

Signed-off-by: Luca Burelli <[email protected]>
@pillo79
Copy link
Collaborator Author

pillo79 commented Sep 3, 2024

Rebased onto current main to include #77473, then pushed v3:

  • moved section identification to llext_map_sections
  • added support for .fini_array as well
  • added llext_bringup and llext_teardown APIs to manually call setup / cleanup functions
  • moved tests to separate PR (llext: add tests for init arrays #77941) to simplify review.

* referenced at most once in the ELF file.
*/
LOG_ERR("Region %d redefined", mem_idx);
return -ENOEXEC;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this be caught in line 211?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, line 211 checks that the ELF section (ldr->sect_map[shndx]) was assigned a region before entering llext_map_sections - I added that log so the whole function prints a full ELF section table, including reserved sections found by llext_find_tables.
This is instead checking if the region has been already defined (ie. ldr->sects[mem_idx] - the array that really should be renamed ldr->regions...).


/* Start extension main function */
LOG_DBG("calling entry function %p(%p)", entry_fn, user_data);
entry_fn(user_data);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you wrote "if desired" in the commit description. Should you check for entry_fn == NULL then?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, commit message is a leftover from the previous iteration, when llext_bootstrap was the only way to call init functions. I have since changed code, comments and documentation to make it clear it's not optional anymore; if you only want to init an extension, you now have llext_bringup.
Will fix if I have to do any code change 👍

@nashif nashif merged commit fca654c into zephyrproject-rtos:main Sep 6, 2024
24 checks passed
@pillo79 pillo79 deleted the pr-init-arrays-v3 branch October 2, 2024 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: llext Linkable Loadable Extensions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants