4

Consider the program:

main.c

#include <stdlib.h>

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "call abort;"
    "ret;"
);

int main(int argc, char **argv) {
    if (argv[1][0] == '0') {
        abort();
    } else if (argv[1][0] == '1') {
        __asm__("call abort");
    } else {
        my_asm_func();
    }
}

Which I compile as:

gcc -ggdb3 -O0 -o main.out main.c

Then I have:

$ ./main.out 0; echo $?
Aborted (core dumped)
134
$ ./main.out 1; echo $?
Aborted (core dumped)
134
$ ./main.out 2; echo $?
Segmentation fault (core dumped)
139

Why do I get the segmentation fault only for the last run, and not an abort signal as expected?

man 7 signal:

   SIGABRT       6       Core    Abort signal from abort(3)
   SIGSEGV      11       Core    Invalid memory reference

confirms the signals due to the 128 + SIGNUM rule.

As a sanity check I also tried to make other function calls from assembly as in:

#include <stdlib.h>

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "lea puts_message(%rip), %rdi;"
    "call puts;"
    "ret;"
    "puts_message: .asciz \"hello puts\""
);

int main(void) {
    my_asm_func();
}

and that did work and print:

hello puts

Tested in Ubuntu 19.04 amd64, GCC 8.3.0, glibc 2.29.

I also tried it in an Ubunt Ubuntu 18.04 docker, and the results were the same, except that the program outputs when running:

./main.out: Symbol `abort' causes overflow in R_X86_64_PC32 relocation          
./main.out: Symbol `abort' causes overflow in R_X86_64_PC32 relocation

which feels like a good clue.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
  • The relocation overflow error is a separate problem: you needed to use `call abort@plt` or `call *abort@GOTPCREL(%rip)`. IDK why you don't get that with Ubuntu 19.04. – Peter Cordes May 27 '19 at 17:57

1 Answers1

5

In this code that defines a function at global scope (with basic assembly):

void my_asm_func(void);

__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "call abort;"
    "ret;"
);

You violate one of the x86-64(AMD64) System V ABI rules that requires 16 byte stack alignment (may be higher depending on the parameters) at a point just before a CALL is made.

3.2.2 The Stack Frame

In addition to registers, each function has a frame on the run-time stack. This stack grows downwards from high addresses. Figure 3.3 shows the stack organization.

The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32) when control is transferred to the function entry point. The stack pointer, %rsp, always points to the end of the latest allocated stack frame.

Upon entry to a function the stack will be misaligned by 8 because the 8 byte return address is now on the stack. To align the stack back on a 16 byte boundary subtract 8 from RSP at the beginning of the function and add 8 back to RSP when finished. You can also just push any register like RBP at the beginning and pop it after to get the same effect.

This version of the code should work:

void my_asm_func(void);

__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "push %rbp;"
    "call abort;"
    "pop %rbp;"
    "ret;"
);

Regarding this code that happened to work:

__asm__("call abort");

The compiler likely generated the main function in such away that the stack was aligned on a 16 byte boundary prior to this call so it happened to work. You shouldn't rely on this behavior. There are other potential issues with this code, but don't present as a failure in this case. The stack should be properly aligned before the call; you should be concerned in general about the red zone; and you should specify all the volatile registers in the calling conventions as clobbers including RAX/RCX/RDX/R8/R9/R10/R11, the FPU registers, and the SIMD registers. In this case abort never returns so this isn't an issue related to your code.

The red-zone is defined in the ABI this way:

The 128-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers.8 Therefore, functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is known as the red zone.

It is generally a bad idea to call a function in inline assembly. An example of calling printf can be found in this other Stackoverflow answer which shows the complexities of doing a CALL especially in 64-bit code with red-zone. David Wohlferd's Dont Use Inline Asm is always a good read.


This code happened to work:

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "lea puts_message(%rip), %rdi;"
    "call puts;"
    "ret;"
    "puts_message: .asciz \"hello puts\""
);

but you were probably lucky that puts didn't need proper alignment and you happened to get no failure. You should be aligning the stack before calling puts as described earlier with the my_asm_func that called abort. Ensuring compliance with the ABI is the key to ensuring code will work as expected.


Regarding the relocation errors, that is probably because the version of Ubuntu being used is using Position Independent Code (PIC) by default for GCC code generation. You could fix the issue by making the C library calls though the Procedure Linkage Table by appending @plt to the function names you CALL. Peter Cordes wrote a related Stackoverflow answer on this topic.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
  • 2
    Thanks Michael! I knew about those stack requirements but forgot to think about them! – Ciro Santilli OurBigBook.com May 27 '19 at 14:57
  • 1
    @Ciro: An easy solution for a stand-alone function would be a tail-call with a `jmp` instead of `call`, if you don't need it to be visible in the backtrace printed by `abort()`. Speaking of which; without CFI directives to create `.eh_frame` metadata you probably broke abort()'s ability to backtrace and dump the call stack. – Peter Cordes May 27 '19 at 17:59
  • @Michael: `abort()` is a noreturn function; you don't have to care about destroying the caller's registers. That section is a good point in *general* about calling functions from inline-asm, but `asm("call abort")` maybe isn't the right heading for it. – Peter Cordes May 27 '19 at 18:01
  • @PeterCordes : I happened to state _In this case `abort` **never returns** so this is likely not an issue._ I swill correct something about that though in that I did mean to say "isn't an issue" – Michael Petch May 27 '19 at 18:03
  • As for the tail call (I considered mentioning it and decided against it), I'll let your comment stand, it doesn't do anything but dilute the real issue. As well if someone comes by and copies and pastes my example and places other code between the push and pop then their code isn't going to break. – Michael Petch May 27 '19 at 18:05
  • I just noticed that caveat at the bottom of a paragraph. The stack-alignment point is good; it's undocumented behaviour that gcc really likes to align the stack by 16 for function bodies, so in practice it tends to work. But yes, that applies even to noreturn functions. Maybe split that section up into 2 sections: one introduced with `asm("call abort")`, and another introduced with `asm("call function_that_returns")` to talk about that case, while highlighting the difference between noreturn functions. – Peter Cordes May 27 '19 at 18:07
  • 1
    @CiroSantilli新疆改造中心996ICU六四事件: update: `abort()` doesn't actually do a stack backtrace on its own. And with Michael's `push %rbp`, GDB is still able to back-trace through `my_asm_func` to the call site in `main`, even though I compiled with `-O3` (implying `-fomit-frame-pointer`). So I was wrong about non-tailcall breaking backtraces, apparently. I guess GDB is clever here. But with a `sub $128, %rsp` before the `call abort`, GDB gets lost and can't find the caller. Or if you push a dummy register like `push %rcx` instead of RBP, GDB thinks there's a `_nl_current_default_domain` before main – Peter Cordes May 27 '19 at 18:27
  • 1
    @PeterCordes thanks for this info. I once got crazy and to understand GDB 's backtrace algorithm a bit. But then I got lazy and stopped. Whoever understands it should write something at https://stackoverflow.com/questions/4349162/how-gdb-reconstructs-stacktrace-for-c :-) – Ciro Santilli OurBigBook.com May 27 '19 at 22:07