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

Port to other architectures #30

Open
inconstante opened this issue Aug 14, 2021 · 3 comments
Open

Port to other architectures #30

inconstante opened this issue Aug 14, 2021 · 3 comments

Comments

@inconstante
Copy link
Contributor

inconstante commented Aug 14, 2021

Currently, Libpulp only supports x86_64. Adding support for more architectures requires a few changes and a couple of new files. This issue tries to summarize what's needed:

1- Calling libpulp functions from ptrace:

Libpulp is like GDB. It attaches to running processes with ptrace and does stuff, such as calling functions in the target process. Calling functions from ptraced processes is not like calling functions in its own memory space, so Libpulp must hijack a running thread, stealing its context and diverting execution to wherever it wants. However, to regain control, it can't rely on regular ret instructions; instead, it must cause a software trap. On x86, that is achieved with the int3 instruction, such as the following snippet, from lib/ulp_interface.S:

__ulp_trigger:
    nop
    nop
    call    __ulp_apply_patch@PLT
    int3

where __ulp_trigger is the function Libpulp wants to call. Notice how it ends with int3, not ret.

A new port must provide a new lib/ulp_interface.S implementation.

2- Selecting between versions of a live patched function:

After a live patch has been applied, live patched functions have their prologues altered (see item 3, below), so that, instead of running the actual code of the function, execution gets diverted to a small block of code in its preamble that: saves the contents of %rdi (a caller-saved register) onto the stack; writes an index value to it; calls a live patch version selection routine, which performs its computation, then returns the result in %r11 (x86's scratch register on AMD64's ABI supplement for SysV).

Since the version selection routine executes between calls, it must preserve all registers, even caller-saved ones, specially those used in parameter passing. Otherwise, when the target function gets finally executed, the parameters would be wrong. In Libpulp, the saving and restoring of registers happens in lib/ulp_prologue.S.

Step-by-step, this is what happens in a call to a live patched function:

  1. The application calls the library function, e.g. foo;
  2. foo's prologue saves %rdi on the stack;
  3. foo's prologue writes a [hardcoded] index value to %rdi;
  4. foo's prologue jumps to __ulp_prologue;
  5. __ulp_prologue saves all caller-saved registers;
  6. __ulp_prologue calls __ulp_manage_universes;
  7. __ulp_manage_universes returns the address of the target function on %r11;
  8. __ulp_prologue restores all saved registers;
  9. __ulp_prologue jumps through %r11, which contais the address of the right version of foo.

A new port must provide a similar mechanism for its target architecture.

3- Patching function prologues

As mentioned above, live patched functions have their prologues altered, so that, instead of execution the function, as they normally would, they call a version selection routine. This function selection routine takes a single argument, an index into Libpulp's data structures, which contain references to all versions of the live patched functions.

However, when a live patchable process starts, the prologues of live patchable functions contain nop instructions, so that the regular functions execute normally. When a live patch is applied, these nops get replaced with a small block of code that diverts execution to the version selection routine. On x86_64, these blocks of code look like the following snippet:

prologue:
    push    %rdi
    movq    $index, %rdi
    jmp     0x0(%rip)
function:                  <-- this is the actual entry point to the function
    jmp     <prologue>

In the current implementation, Libpulp only knows how to write x64_64 code. The snippet above is written by ulp_patch_prologue_layout (also check ulp_prologue and ulp_patch_addr_absolute).

A new port must implement a mechanism that similarly patches function prologues.

4- compile specifically to emit a preamble and nops at function starts

on x86-64 that is GCCs -fpatchable-function-entry=N,M option. Using libpulp on other architecture means that this flag
needs to be supported on that architecture.

@susematz
Copy link
Collaborator

4 - compile specifically to emit a preamble and nops at function starts

on x86-64 that is GCCs -fpatchable-function-entry=N,M option. Using libpulp on other architecture means that this flag
needs to be supported on that architecture.

@susematz
Copy link
Collaborator

susematz commented Sep 3, 2021

More items:

  • size of ELF hash table entry is 64 bit for s390x (and alpha), otherwise 32 bit
  • all Elf64_xxx use in libpulp and tools need changes for 32bit

@giulianobelinassi
Copy link
Collaborator

S390x port is desired.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants