In Linux, How to Do System Calls Through Gnu Arm Assembly

In linux, how to do system calls through GNU ARM assembly

You can take an approach like Android's Bionic and generate sys call stubs via some metadata and a script or use Bionic's directly.

Below is from Bionic's libc/SYSCALLS.TXT

# this file is used to list all the syscalls that will be supported by
# the Bionic C library. It is used to automatically generate the syscall
# stubs, the list of syscall constants (__NR_xxxx) and the content of <linux/_unistd.h>
#
# each non comment line has the following format:
#
# return_type func_name[:syscall_name[:call_id]]([parameter_list]) (syscall_number|"stub")
#
# note that:
# - syscall_name correspond to the name of the syscall, which may differ from
# the exported function name (example: the exit syscall is implemented by the _exit()
# function, which is not the same as the standard C exit() function which calls it)
# The call_id parameter, given that func_name and syscall_name have
# been provided, allows the user to specify dispatch style syscalls.
# For example, socket() syscall on i386 actually becomes:
# socketcall(__NR_socket, 1, *(rest of args on stack)).
#
# - each parameter type is assumed to be stored on 32 bits, there is no plan to support
# 64-bit architectures at the moment
#
# - it there is "stub" instead of a syscall number, the tool will not generate any
# assembler template for the syscall; it's up to the bionic implementation to provide
# a relevant C stub
#
# - additionally, if the syscall number is different amoung ARM, and x86, MIPS use:
# return_type funcname[:syscall_name](parameters) arm_number,x86_number,mips_number
#
# the file is processed by a python script named gensyscalls.py
#

# process management
void _exit:exit_group (int) 248,252,246
void _exit_thread:exit (int) 1
pid_t __fork:fork (void) 2

<skipped rest of the file>

In-line assembly to perform syscall with return value in ARM

Fix : The fix is to use "-fomit-frame-pointer" directive when compiling using arm-linux-gnueabihf-gcc (probably for other flavors as well).

$arm-linux-gnueabihf-gcc {YOUR FILE NAME}.c -fomit-frame-pointer -o {YOUR OUTPUT FILE NAME}

Thank you guys for all the comments. So, for the record, the above code logic in the question, works. As far as I understand it, the issue was that the compiler for some reason uses r7 as frame pointer. You can keep adding arguments as input and create a more general purpose syscall implementation using inline assembly. Consider what people have said in the comments for better implementations.

Pseudocode

returntype my_syscall(int syscallnumber,type arg1,type arg2,..)
{
int output_variable = 0;
__asm__ volatile
(
"mov r7,[sysnum]\n\t"
"mov r0,[var1]\n\t"
"mov r1,[var2]\n\t"
//continue similarly for more input arguments etc
"svc 0\n\t"
"mov %0,r0\n\t" //Return value of syscall will be available in r0 for ARM EABI
:"=r"(output_variable)
:[sysnum]"r"(syscallnumber),[var1]"r"(arg1),[var2]"r"(arg2) //and so on for other arguments
:"memory" //clobber others based on usage
);
return output_variable;
}

Backstory (For anyone interested) : The same code(the one which is present in the question) with slight variations worked when I implemented a simple "Hello World" string write (changes included different syscall number and arguments). But it wouldn't work when I called my custom syscall which returned a needed value (Alternatively, you can design the syscall to return values using arguments instead of return instruction to avoid this whole issue. I didn't do it because then I would have to recompile the whole kernel but I digress). I tested my syscall with inbuilt syscall() function and it worked perfectly. But MY implementation of the syscall() function was unable to call MY custom syscall.
I tried register variables , I tried different instructions and clobbered r0, memory etc.(before I asked the question here) but nothing seemed to work. Then I tried clobbering r7 and the compiler said you can't do that. So that is when I found out the r7 register was being used for something else as well. When I googled the error I found the above fix.
Ironically, after the fix it worked without clobbering r7 or anything else. lol.

What is the interface for ARM system calls and where is it defined in the Linux kernel?

In ARM world, you do a software interrupt (mechanism to signal the kernel) by supervisor call / svc (previously called SWI).

ARM assembly (UAL) syntax looks like this:

SVC{<c>}{<q>} {#}<imm>

(In Linux you need to pass #0)

You should cheat from other projects like bionic or uClibc.

Minimal assembly program ARM

The C language starts at main() but for C to work you in general need at least a minimal bootstrap. For example before main can be called from the C bootstrap

1) stack/stackpointer
2) .data initialized
3) .bss initalized
4) argc/argv prepared

And then there is C library which there are many/countless C libraries and each have their own designs and requirements to be satisfied before main is called. The C library makes system calls into the system so this starts to become system (operating system, Linux, Windows, etc) dependent, depending on the design of the C library that may be a thin shim or heavily integrated or somewhere in between.

Likewise for example assuming that the operating system is taking the "binary" (binary formats supported by the operating system and rules for that format are defined by the operating system and the toolchain must conform likewise the C library (even though you see the same brand name sometimes assume toolchain and C library are separate entities, one designed to work with the other)) from a non volatile media like a hard drive or ssd and copying the relevant parts into memory (some percentage of the popular, supported, binary file formats, are there for debug or file format and not actually code or data that is used for execution).
So this leaves a system level design option of does the binary file format indicate .data, .bss, .text, etc (note that .data, .bss, .text are not standards just convention most people know what that means even if a particular toolchain did not choose to use those names for sections or even the term sections).

If so the operating systems loader that takes the program and loads it into memory can choose to put .data in the right place and zero .bss for you so that the bootstrap does not have to. In a bare-metal situation the bootstrap would normally handle the read/write items because it is not loaded from media by some other software it is often simply in the address space of the processor on a rom of some flavor.

Likewise argv/argc could be handled by the operating systems tool that loads the binary as it had to parse out the location of the binary from the command line assuming the operating system has/uses a command line interface. But it could as easily simply pass the command line to the bootstrap and the bootstrap has to do it, these are system level design choices that have little to do with C but everything to do with what happens before main is called.

The memory space rules are defined by the operating system and between the operating system and the C library which often contains the bootstrap due to its intimate nature but I guess the C library and bootstrap could be separate. So linking plays a role as well, does this operating system support protection is it just read/write memory and you just need to spam it in there or are there separate regions for read/only (.text, .rodata, etc) and read/write (.data, .bss, etc). Someone needs to handle that, linker script and bootstrap often have a very intimate relationship and the linker script solution is specific to a toolchain not assumed to be portable, why would it, so while there are other solutions the common solution is that there is a C library with a bootstrap and linker solution that are heavily tied to the operating system and target processor.

And then you can talk about what happens after main(). I am happy to see you are using ARM not x86 to learn first, although aarch64 is a nightmare for a first one, not the instruction set just the execution levels and all the protections, you can go a long long way with this approach but there are some things and some instructions you cannot touch without going bare metal. (assuming you are using a pi there is a very good bare-metal forum with a lot of good resources).

The gnu tools are such that binutils and gcc are separate but intimately related projects. gcc knows where things are relative to itself so assuming you combined gcc with binutils and glibc (or you just use the toolchain you found), gcc knows relative to where it executed to find these other items and what items to pass when it calls the linker (gcc is to some extent just a shell that calls a preprocessor a compiler the assembler then linker if not instructed not to do these things). But the gnu binutils linker does not. While as distasteful as it feels to use, it is easier just to

gcc test.o -o test

rather than figure out for that machine that day what all you need on the ld command line and what paths and depending on design the order on the command line of the arguments.

Note you can probably get away with this as a minimum

.global main
.type main, %function
main:
mov w0, 5 //move 5 to register w0
ret //return

or see what gcc generates

unsigned int fun ( void )
{
return 5;
}

.arch armv8-a
.file "so.c"
.text
.align 2
.p2align 4,,11
.global fun
.type fun, %function
fun:
mov w0, 5
ret
.size fun, .-fun
.ident "GCC: (GNU) 10.2.0"

I am used to seeing more fluff in there:

    .arch armv5t
.fpu softvfp
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 2
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "so.c"
.text
.align 2
.global fun
.syntax unified
.arm
.type fun, %function
fun:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
mov r0, #5
bx lr
.size fun, .-fun
.ident "GCC: (Ubuntu/Linaro 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609"
.section .note.GNU-stack,"",%progbits

Either way, you can look up each of the assembly language items and decide if you really need them or not, depends in part on if you feel the need to use a debugger or binutils tools to tear apart the binary (do you really need to know the size of fun for example in order to learn assembly language?)

If you wish to control all of the code and not link with a C library you are more than welcome to you need to know the memory space rules for the operating system and create a linker script (the default one may in part be tied to the C library and is no doubt overly complicated and not something you would want to use as a starting point). In this case being two instructions in main you simply need the one address space valid for the binary, however the operating system enters (ideally using the ENTRY(label), which could be main if you want but often is not _start is often found in linker scripts but is not a rule either, you choose. And as pointed out in comments you would need to make the system call to exit the program. System calls are specific to the operating system and possibly version and not specific to a target (ARM), so you would need to use the right one in the right way, very doable, your whole project linker script and assembly language could be maybe a couple dozen lines of code total. We are not here to google those for you so you would be on your own for that.

Part of your problem here is you are searching for compiler solutions when the compiler in general has absolutely nothing to do with any of this. A compiler takes one language turns it into another language. An assembler same deal but one is simple and the other usually machine code, bits. (some compilers output bits not text as well). It is equivalent to looking up the users manual for a table saw to figure out how to build a house. The table saw is just a tool, one of the tools you need, but just a generic tool. The compiler, specific gnu's gcc, is generic it does not even know what main() is. Gnu follows the Unix way so it has a separate binutils and C library, separate developments, and you do not have to combine them if you do not want to, you can use them separately. And then there is the operating system so half your question is buried in operating system details, the other half in a particular C library or other solution to connect main() to the operating system.

Being open source you can go look at the bootstrap for glibc and others and see what they do. Understanding this type of open source project the code is nearly unreadable, much easier to disassemble sometimes, YMMV.

You can search for the Linux system calls for arm aarch64 and find the one for exit, you can likely see that the open source C libraries or bootstrap solutions you find that are buried under what you are using today, will call exit but if not then there is some other call they need to make to return back to the operating system. It is unlikely it is a simple ret with a register that holds a return value, but technically that is how someone could choose to do it for their operating system.

I think you will find for Linux on arm that Linux is going to parse the command line and pass argc/argv in registers, so you can simply use them. And likely going to prep .data and .bss so long as you build the binary correctly (you link it correctly).

How to invoke a system call via syscall or sysenter in inline assembly?

First of all, you can't safely use GNU C Basic asm(""); syntax for this (without input/output/clobber constraints). You need Extended asm to tell the compiler about registers you modify. See the inline asm in the GNU C manual and the inline-assembly tag wiki for links to other guides for details on what things like "D"(1) means as part of an asm() statement.

You also need asm volatile because that's not implicit for Extended asm statements with 1 or more output operands.


I'm going to show you how to execute system calls by writing a program that writes Hello World! to standard output by using the write() system call. Here's the source of the program without an implementation of the actual system call :

#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size);

int main(void)
{
const char hello[] = "Hello world!\n";
my_write(1, hello, sizeof(hello));
return 0;
}

You can see that I named my custom system call function as my_write in order to avoid name clashes with the "normal" write, provided by libc. The rest of this answer contains the source of my_write for i386 and amd64.

i386

System calls in i386 Linux are implemented using the 128th interrupt vector, e.g. by calling int 0x80 in your assembly code, having set the parameters accordingly beforehand, of course. It is possible to do the same via SYSENTER, but actually executing this instruction is achieved by the VDSO virtually mapped to each running process. Since SYSENTER was never meant as a direct replacement of the int 0x80 API, it's never directly executed by userland applications - instead, when an application needs to access some kernel code, it calls the virtually mapped routine in the VDSO (that's what the call *%gs:0x10 in your code is for), which contains all the code supporting the SYSENTER instruction. There's quite a lot of it because of how the instruction actually works.

If you want to read more about this, have a look at this link. It contains a fairly brief overview of the techniques applied in the kernel and the VDSO. See also The Definitive Guide to (x86) Linux System Calls - some system calls like getpid and clock_gettime are so simple the kernel can export code + data that runs in user-space so the VDSO never needs to enter the kernel, making it much faster even than sysenter could be.


It's much easier to use the slower int $0x80 to invoke the 32-bit ABI.

// i386 Linux
#include <asm/unistd.h> // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"int $0x80"
: "=a" (ret)
: "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
: "memory" // the kernel dereferences pointer args
);
return ret;
}

As you can see, using the int 0x80 API is relatively simple. The number of the syscall goes to the eax register, while all the parameters needed for the syscall go into respectively ebx, ecx, edx, esi, edi, and ebp. System call numbers can be obtained by reading the file /usr/include/asm/unistd_32.h.

Prototypes and descriptions of the functions are available in the 2nd section of the manual, so in this case write(2).

The kernel saves/restores all the registers (except EAX) so we can use them as input-only operands to the inline asm. See What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64

Keep in mind that the clobber list also contains the memory parameter, which means that the instruction listed in the instruction list references memory (via the buf parameter). (A pointer input to inline asm does not imply that the pointed-to memory is also an input. See How can I indicate that the memory *pointed* to by an inline ASM argument may be used?)

amd64

Things look different on the AMD64 architecture which sports a new instruction called SYSCALL. It is very different from the original SYSENTER instruction, and definitely much easier to use from userland applications - it really resembles a normal CALL, actually, and adapting the old int 0x80 to the new SYSCALL is pretty much trivial. (Except it uses RCX and R11 instead of the kernel stack to save the user-space RIP and RFLAGS so the kernel knows where to return).

In this case, the number of the system call is still passed in the register rax, but the registers used to hold the arguments now nearly match the function calling convention: rdi, rsi, rdx, r10, r8 and r9 in that order. (syscall itself destroys rcx so r10 is used instead of rcx, letting libc wrapper functions just use mov r10, rcx / syscall.)

// x86-64 Linux
#include <asm/unistd.h> // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"syscall"
: "=a" (ret)
// EDI RSI RDX
: "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
: "rcx", "r11", "memory"
);
return ret;
}

(See it compile on Godbolt)

Do notice how practically the only thing that needed changing were the register names, and the actual instruction used for making the call. This is mostly thanks to the input/output lists provided by gcc's extended inline assembly syntax, which automagically provides appropriate move instructions needed for executing the instruction list.

The "0"(callnum) matching constraint could be written as "a" because operand 0 (the "=a"(ret) output) only has one register to pick from; we know it will pick EAX. Use whichever you find more clear.


Note that non-Linux OSes, like MacOS, use different call numbers. And even different arg-passing conventions for 32-bit.

ARM inline asm: exit system call with value read from memory

Extended-asm syntax requires writing %% to get a single % in the asm output. e.g. for x86:

asm("inc %eax")                // bad: undeclared clobber
asm("inc %%eax" ::: "eax"); // safe but still useless :P

%r7 is treating r7 as an operand number. As commenters have pointed out, just omit the %s, because you don't need them for ARM, even with GNU as.


Unfortunately, there doesn't seem to be a way to request input operands in specific registers on ARM, the way you can for x86. (e.g. "a" constraint means eax specifically).

You can use register int var asm ("r7") to force a var to use a specific register, and then use an "r" constraint and assume it will be in that register. I'm not sure this is always safe, or a good idea, but it appears to work even after inlining. @Jeremy comments that this technique was recommended by the GCC team.

I did get some efficient code generated, which avoids wasting an instruction on a reg-reg move:

See it on the Godbolt Compiler Explorer:

__attribute__((noreturn)) static inline void ASM_EXIT(int status)
{
register int status_r0 asm ("r0") = status;
register int callno_r7 asm ("r7") = 1;
asm volatile("swi #0\n"
:
: "r" (status_r0), "r" (callno_r7)
: "memory" // any side-effects on shared memory need to be done before this, not delayed until after
);
// __builtin_unreachable(); // optionally let GCC know the inline asm doesn't "return"
}

#define GET_STATUS() (*(int*)(some_address)) //gets an integer from an address

void foo(void) { ASM_EXIT(12); }
push {r7} @ # gcc is still saving r7 before use, even though it sees the "noreturn" and doesn't generate a return
movs r0, #12 @ stat_r0,
movs r7, #1 @ callno,
swi #0
# yes, it literally ends here, after the inlined noreturn

void bar(int status) { ASM_EXIT(status); }
push {r7} @
movs r7, #1 @ callno,
swi #0 # doesn't touch r0: already there as bar()'s first arg.

Since you always want the value read from memory, you could use an "m" constraint and include a ldr in your inline asm. Then you wouldn't need the register int var asm("r0") trick to avoid a wasted mov for that operand.

The mov r7, #1 might not always be needed either, which is why I used the register asm() syntax for it, too. If gcc wants a 1 constant in a register somewhere else in a function, it can do it in r7 so it's already there for the ASM_EXIT.


Any time the first or last instructions of a GNU C inline asm statement are mov instructions, there's probably a way to remove them with better constraints.



Related Topics



Leave a reply



Submit