Avatar

LIEF RTTI & Exceptions

ionicons-v5-k Romain Thomas February 13, 2022
Wave

try {

When we started to develop LIEF, we choose to manage errors through the C++ exceptions as it is widely spread in Java.

However, with a little hindsight it was not the best choice in the design of LIEF.

First off, LIEF is a library and the API functions that throw exceptions are not compatible with library’s users that are not using exceptions (e.g with the -fno-exceptions flag). It is also considered as a bad practice quoting from C++ Coding Standards:

C++ Coding Standards: Item 62

“Don't throw stones into your neighbor's garden: There is no ubiquitous binary standard for C++ exception handling.”

For instance, the function LIEF::ELF::Binary::get_section(const std::string& name) threw an exception if the section were not found. To avoid raising the exception, the API exposes helpers that can be used to check – beforehand – that it will not take the exception path:

 1if (bin.has_section(".toto")) {
 2  auto& sec = bin.get_section(".toto"); // Ok no exception
 3}
 4
 5// With exception:
 6try {
 7  bin.get_section(".toto");
 8} catch (const std::exception&) {
 9  // .toto does not exist :(
10}

Actually, the has_<element> / get_<element> pattern hides another issue: the performances.

Basically, has_section(...) iterates over the list of the sections to check if a section with the given name exists and get_section() iterates again on this list to access the section. The code performs twice the same iteration. This is not a big deal for the ELF sections as they are quite small but it can be problematic for large sequences like the symbols table.

In LIEF v0.12.0 we changed the API of these functions to return a pointer on these objects instead of a reference. If the item can’t be found, it returns a nullptr.

The API contract of these functions is changing from raising an exception into returning a nullptr. The documentation has been updated accordingly and the list of the functions which have changed are listed here

As a result, the previous code can be re-written as follows:

1if (bin.has_section(".toto")) {
2  auto* sec = bin.get_section(".toto"); // Non nullptr instead of a reference
3}
4
5// Or:
6if (auto* sec = bin.get_section(".toto")) {
7  // ...
8}

This kind of API change is doable and meaningful for functions that aim at returning an optional object but it is less meaningful to transform a function like:

1uint64_t Binary::virtual_address_to_offset(...) { ... }

that returns an integer (while still potentially raising an exception).

In LIEF 0.12.0, this kind of function still raises an exception but in the next version (LIEF v0.13.0) the returned value will be wrapped by Boost’s Leaf1 such as the returned type will become:

 1// Future returned type
 2result<uint64_t> Binary::virtual_address_to_offset(...) { ... }
 3
 4// To use it:
 5auto res = bin.virtual_address_to_offset();
 6if (!res) {
 7  // Error
 8} else {
 9  uint64_t val = res.value(); // or val = *res
10}

In LIEF v0.12.0 only internal/private functions associated with the Parser/Builder module are using this mechanism and we plan to move to this mechanism in the public API2 in LIEF v0.13.0.

Warning

Boost LEAF is required in the public headers of LIEF. If you find conflicts, compilation issues, integration issues, or you think that it is a bad idea, please let us know before it becomes the default interface to manage errors.

The RTTI

LIEF also relies on the RTTI information which includes calling functions like typeid() or dynamic_cast<>(). For instance, to check if a Mach-O’s LoadCommand exists, the main MachO::Binary class calls at some point this helper:

 1template<class T>
 2bool Binary::has_command() const {
 3  static_assert(std::is_base_of<LoadCommand, T>::value,
 4                "Require inheritance from 'LoadCommand'");
 5
 6  const auto it_cmd = std::find_if(
 7      std::begin(commands_), std::end(commands_),
 8      [] (const LoadCommand* command) {
 9        return typeid(T) == typeid(*command);
10      });
11
12  return it_cmd != std::end(commands_);
13}

This code generates extra data for the RTTI information of the LoadCommand objects which can be perfectly fine. Actually, this RTTI information is redundant as the type of a Mach-O’s LoadCommand is already stored in the class itself:

1class LoadCommand {
2  ...
3  private:
4  LOAD_COMMAND_TYPES command_;
5};

So instead of having these redundant RTTI, we implemented a LLVM-like RTTI3 based on classof() and that uses the already present command_ attribute. In the end, the previous has_command() can be updated as follows:

 1template<class T>
 2bool Binary::has_command() const {
 3  static_assert(std::is_base_of<LoadCommand, T>::value,
 4                "Require inheritance from 'LoadCommand'");
 5  const auto it_cmd = std::find_if(
 6      std::begin(commands_), std::end(commands_),
 7      [] (const LoadCommand* command) {
 8        return T::classof(command);
 9      });
10  return it_cmd != std::end(commands_);
11}

We applied this pattern for the LIEF’s object where typeid was present and as a result, we managed to completely remove this function as it was redundant with an existing attribute.

} catch (const std::length_error&) {


We welcome feedback on these changes – whether positive or negative – as it impacts the public API.

Thank you for reading!

}


  1. See the section Error Handling of the documentation for more details. ↩︎

  2. It still keeps the public headers compliant with C++11 ↩︎

  3. https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html ↩︎

Avatar
Romain Thomas Posted on February 13, 2022