
Inspiration
Thoroughly enjoyed CSCI 2400 Computer Systems, and have wanted to try working with ELF files for a while. Making a reflection engine is a really cool thing to do with them.
What it does
This project provides files that can be included in any C++ project running on Linux that provide the ability for the program to identify and utilize data and function objects from just a string, without having to manually register each attribute.
How I built it
There are two classes that power this project; Reflection and ElfReader. ElfReader reads the ELF file describing the running process, and provides the symbols to Reflection. Reflection accepts arguments from the developer/user, and makes meaning from the symbols provided by ElfReader.
ElfReader
Currently, the ElfReader gets the current process's ELF file by opening /proc/<pid>/exe. This is really just a symlink to the executable elsewhere in the filesystem, but using the pid is an easy way to get an absolute path, which is why we use it.
Once the ELF file is opened, ElfReader parses the Section Header Table to identify the SHT_DYNSYM entry, and makes a list of the SHT_STRTAB and SHT_SYMTAB entries.
ElfReader also provides a function to lookup a symbol by a string, std::vector<Elf64_Sym*>* ElfReader::lookupSymbol(char *). This method goes through every entry in SHT_DYNSYM and resolves its name by looking it up in one of the SHT_STRTABs, and de-mangling it. If the name that we're looking for exists in the resolved name, the symbol is added to a list of matches, which is returned when all of the entries have been analyzed.
Reflection
The Reflection Class should be instantiated by the Reflection::FromCurrentProcess(void *main) method, which automatically identifies the process's pid and load the ElfReader. Passing the location of int main(int,char**) allow the class to map the value of a symbol to a location in memory; Reflection will lookup the main symbol and calculate the difference between its value and the address passed to it. This offset is then added to any other symbol's value to determine it's location in memory.
The main method to note in Reflection is int Reflection::exec(char*,void*). This currently allows the user to specify a function that they want to execute via the string method, and supply up to 6 arguments that are either strings or numbers. Due to the early stage of the project, only commands with one . seperator work. The fvalue of the command before . is used to identify an instance of a class, and the value after is used to determine the function to run. If no . is needed, it will try to find a global function.
Passing the arguments to a function is done through inline assembly code. The value is pulled from the string passed to the method and stored in the appropriate register. When all registers have populated, inline assembly code is used to call the function.
Calling functions on an object instantiated from a class is also possible. The identified object is simply passed as the first argument to the function in compliance with the machine's ABI.
Challenges I ran into
Passing arguments to the function was the hardest part, especially functions that were part of an instantiated class. I know that I was going to have to use inline assembly, but it proved to be more challenging than I initially thought. It was easy enough to set the registers to the right value, but doing it at the right time so that they weren't overwritten was difficult.
Accomplishments that I'm proud of
It works!
What I learned
More about ELF file, and how programs are linked and loaded in Linux.
What's next for C++ Runtime Reflection Engine
Investigate Support For MCUs
See if there's a way to also make this work for microcontrollers, where the code is flash directly and the ELF file is not available at runtime. May be limited to applications that have peripheral storage or implement an OS that loads the file.
Reading Debug Symbols
Read debug symbols as well to provide more data and options to the user/developer. This could include access to local variables as well as source code and type matching.
Attach To Another Process
Use the ElfReader to read a different process's ELF file, and use a system call like strace to take control of it.
Load and Link Object Files
Use the ElfReader to read another ELF file. Load that program's code and link it to the existing process, therefore providing methods and functionality from the other object.
Built With
- asm
- c++
- elf
Log in or sign up for Devpost to join the conversation.