Decoding a ’service’ call in Android
First, a random notice. There’s an Android-Internals wiki, which hopefully will eventually start to describe everything we know about Android internals.
Secondly, here’s the most recent version of my openbinder.c code for strace. (Again, combine with tweaks to the makefiles etc. I’ve previously posted).
And thirdly, thanks to Davanum’s discover of the ’service’ command, we can start to craft some nice simple OpenBinder interactions and see exactly how they work behind the scenes. Results are promising! I now understand the messages sent from the service command to the kernel, although I have yet to decode the replies from the kernel. More on that later.
Without further ado, here is a transcript using the above strace code of the following command: service call phone 2 s16 “123″. Captured using simply strace -o /tmp/strace.out service call phone 2 s16 “123″.
- General process startup
- execve(”/system/bin/service”, [”service”, “call”, “phone”, “2″, “s16″, “123″], [/* 8 vars */]) = 0
- getpid() = 751
- syscall_983045(0xb0016b48, 0xb0013760, 0×3e4, 0, 0xbeef9e4c, 0×6, 0, 0xf0005, 0xb0013760, 0, 0, 0xbeef9e34, 0, 0xbeef9de8, 0xb0000d89, 0xb00016ec, 0×10, 0xb0016b48, 0, 0, 0, 0xc8c8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
- gettid() = 751
- sigaction(SIGILL, {0xb0001469, [], SA_RESTART}, {SIG_DFL}, 0) = 0
- sigaction(SIGABRT, {0xb0001469, [], SA_RESTART}, {SIG_DFL}, 0) = 0
- sigaction(SIGBUS, {0xb0001469, [], SA_RESTART}, {SIG_DFL}, 0) = 0
- sigaction(SIGFPE, {0xb0001469, [], SA_RESTART}, {SIG_DFL}, 0) = 0
- sigaction(SIGSEGV, {0xb0001469, [], SA_RESTART}, {SIG_DFL}, 0) = 0
- sigaction(SIGSTKFLT, {0xb0001469, [], SA_RESTART}, {SIG_DFL}, 0) = 0
- open(”libutils.so”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
- open(”/system/lib/libutils.so”, O_RDONLY|O_LARGEFILE) = 3
- lseek(3, -8, SEEK_END) = 425880
- read(3, “\0\0\320\251PRE “, 8) = 8
- mmap2(0xa9d00000, 425984, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xa9d00000
- close(3) = 0
- open(”libz.so”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
- open(”/system/lib/libz.so”, O_RDONLY|O_LARGEFILE) = 3
- lseek(3, -8, SEEK_END) = 57252
- read(3, “\0\0\220\257PRE “, 8) = 8
- mmap2(0xaf900000, 57344, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xaf900000
- close(3) = 0
- open(”libc.so”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
- open(”/system/lib/libc.so”, O_RDONLY|O_LARGEFILE) = 3
- lseek(3, -8, SEEK_END) = 231908
- read(3, “\0\0\340\257PRE “, 8) = 8
- mmap2(0xafe00000, 233472, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xafe00000
- close(3) = 0
- mmap2(0xafe39000, 45056, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0xafe39000
- mprotect(0xafe00000, 221184, PROT_READ|PROT_EXEC) = 0
- open(”libstdc++.so”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
- open(”/system/lib/libstdc++.so”, O_RDONLY|O_LARGEFILE) = 3
- lseek(3, -8, SEEK_END) = 3812
- read(3, “\0\0\320\257PRE “, 8) = 8
- mmap2(0xafd00000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xafd00000
- close(3) = 0
- open(”libm.so”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
- open(”/system/lib/libm.so”, O_RDONLY|O_LARGEFILE) = 3
- lseek(3, -8, SEEK_END) = 131596
- read(3, “\0\0\300\257PRE “, 8) = 8
- mmap2(0xafc00000, 135168, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xafc00000
- close(3) = 0
- mprotect(0xafc00000, 126976, PROT_READ|PROT_EXEC) = 0
- mprotect(0xaf900000, 53248, PROT_READ|PROT_EXEC) = 0
- open(”libcutils.so”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
- open(”/system/lib/libcutils.so”, O_RDONLY|O_LARGEFILE) = 3
- lseek(3, -8, SEEK_END) = 54548
- read(3, “\0\0\260\257PRE “, 8) = 8
- mmap2(0xafb00000, 57344, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xafb00000
- close(3) = 0
- mmap2(0xafb0e000, 61440, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0xafb0e000
- mprotect(0xafb00000, 53248, PROT_READ|PROT_EXEC) = 0
- mprotect(0xa9d00000, 344064, PROT_READ|PROT_EXEC) = 0
- brk(0) = 0xa000
- brk(0xa000) = 0xa000
- brk(0xb000) = 0xb000
- sched_get_priority_min(SCHED_RR) = 1
- sched_get_priority_max(SCHED_RR) = 99
- mprotect(0, 0, PROT_READ|PROT_EXEC) = 0
- gettid() = 751
- syscall_983045(0xbeef9d3c, 0, 0×20, 0, 0xbeeda000, 0xbeef9df0, 0xbeef9e30, 0xf0005, 0xbeef9e30, 0, 0×8858, 0×8854, 0, 0xbeef9d30, 0xafe20a53, 0xafe0949c, 0×60000010, 0xbeef9d3c, 0, 0, 0, 0xc8c8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
- SYS_281(0×1, 0×1, 0, 0xafe40d20, 0xafe371bc) = 3
- SYS_283(0×3, 0xbeef9c9a, 0×13, 0, 0×3) = 0
- SYS_297(0×3, 0xbeef9cd4, 0, 0×1, 0xbeef9cd4) = 4
- mmap2(NULL, 32768, PROT_READ, MAP_SHARED, 4, 0) = 0×40000000
- close(4) = 0
- close(3) = 0
- open(”/data/malloc_debug”, O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
- Open the device for OpenBinder
- open(”/dev/binder”, O_RDWR|O_LARGEFILE) = 3
- fcntl64(3, F_SETFD, FD_CLOEXEC) = 0
- Check what OpenBinder version is in use. The kernel replies 5.
- ioctl(3, BINDER_VERSION, {protocol_version=5}) = 0
- State that the maximum threads we’re happy for OpenBinder to create in our process is 15. (At least, I think that’s what this is doing).
- ioctl(3, BINDER_SET_MAX_THREADS, 15) = 0
- Map OpenBinder’s global memory to a space in our process. Later on, we’ll see what appears in this address space. Note that this is read-only; this space appears to be user for communications from the kernel Binder driver to this service executable, but not for data going from the executable to the server.
- mmap2(NULL, 8388608, PROT_READ, MAP_PRIVATE|MAP_NORESERVE, 3, 0) = 0×40008000
- Get some details about this process, which presumably are going to be needed for the OpenBinder transactions. The process priority is definitely needed, because when a thread makes an OpenBinder request to another process, the request runs within a thread in the destination process at the same priority as the requesting thread.
- getpid() = 751
- getuid32() = 0
- getpid() = 751
- getuid32() = 0
- getpriority(PRIO_PROCESS, 0) = 20
- Start an OpenBinder transaction to talk to the default service.
Specifically, we create a transaction whose target is handle 0. I’m not sure what handle 0 means in OpenBinder, but presumably it’s some general OpenBinder framework or service. Logically, that makes sense, because OpenBinder handles must either refer to some local object (in which case they’re just a pointer) or some remote object… in which case the handle must already have been supplied by the OpenBinder driver in some previous transaction or reply. Since we’re the first, and since it’s clearly not a pointer, handle 0 must mean something special. The request we make has operation code 0. Again, I assume that’s just some general registration request.
The request body has 44 bytes of data, of which none are a reference to any other object or entity in the Binder world, whether in our process or elsewhere.
The contents of the 44 bytes is the string we see containing the word ‘default’. This is the request body - really, the parameters to the function which is being called across the process boundary. The contents of these do not appear to be self-describing, and presumably therefore rely on a mutual knowledge of the interface description. Since I don’t know what this service is, I can’t speculate what data it’s providing. But, I can reveal a few things. The first four bytes are the process ID (0×000002ef == 751). I speculate that the subsequent four bytes of 0×0 are the UID. After that, we’ve got the string length (0×07 bytes) then the string. The strings always seem to end in a null (0×00 0×00) even though they’re length-prefixed. Then we see 0×85,0×2a,0×62,0×73… which is a mystery but I see it (or similar) in lots of these bodies so it would be nice to figure it out. Finally, there’s 0×08 0×00 0×00 0×00, then another empty word (0×00 0×00 0×00 0×00).
The kernel driver replies to say that the transaction was complete. It doesn’t return any data. The purpose of the brNOOP is explained in the OpenBinder documentation available on Dianne Hackborn’s website.
- ioctl(3, BINDER_WRITE_READ, {write_size=40,write_consumed=40,write_buffer=0xa438,read_size=256,read_consumed=8,read_buffer=0xa328,write_data=[bcTRANSACTION({target=0×00000000, cookie=0×0000a204,code=0×00000000, flags=0×00000000,priority=80,data_size=44, offsets_size=0, data={buffer=0×0000a540,offsets=0×0000a300,*buffer= [0xef,0×02,0×00,0×00,0×00,0×00,0×00,0×00,0×07,0×00,0×00,0×00,0×64,0×00,0×65,0×00,0×66,0×00,0×61, 0×00,0×75,0×00,0×6c,0×00,0×74,0×00,0×00,0×00,0×85,0×2a,0×62,0×73,0×08,0×00,0×00,0×00,0×00,0×00,0×00,0×00,0×00,0×00,0×00,0×00, “…………d.e.f.a.u.l.t….*bs…………”], *offsets=[”"]}})],read_data={[brNOOP,brTRANSACTION_COMPLETE]}) = 0
- And now we start another OpenBinder transaction. In this one, we (the service process) send nothing, but we offer up a buffer of 256 bytes to receive a reply. This ioctl will then block until our target has prepared the reply, at which point the kernel driver will fill in our buffer and give the reply to us. Sadly, this is where my strace hacks aren’t good enough. Although I can show the data sent from the process to the kernel, I can’t show the reply received… the address is 0×4008000, which is at the start of the mmap’ped /dev/binder memory. strace is not happy to give me that memory, and I haven’t yet looked into why. Maybe I’ll get there in the end.
Meanwhile, though, we can see a few things. The reply is 24 bytes long, of which there is one flat_binder_object at a certain offset into the buffer. That flat_binder_object will contain a handle to some sort of service discovery object in some other process. At a guess, that handle is going to be 0×01.
- ioctl(3, BINDER_WRITE_READ, {write_size=0,write_consumed=0,write_buffer=0xa438,read_size=256,read_consumed=44,read_buffer=0xa328, write_data=[],read_data={[brNOOP,brREPLY({flags=0×00000000,priority=80,data_size=24,offsets_size=4, data={buffer=0×40008000,offsets=0×40008018,*buffer=[…]}})]}) = 0
- Our next OpenBinder interaction says we want to keep a reference to that handle - 0×01. We also acquire it (I’m not sure what that means) and state that we’ve finished with the buffer we read from the global memory. All Binder things are reference-counted… presumably the fact it was in a buffer meant there was a reference to the service discovery object, then when we INCREF’ed, we claimed another one, and destroyed the buffer thus removing the original one. At any rate, the reference count never went to 0 so Binder won’t have destroyed the original object.
- ioctl(3, BINDER_WRITE_READ, {write_size=24,write_consumed=24,write_buffer=0xa438,read_size=0,read_consumed=0,read_buffer=0xa9d2cf57, write_data=[bcINCREFS(target=0×00000001),bcACQUIRE(target=0×00000001),bcFREE_BUFFER(ptr=0×40008000)],read_data={[]}) = 0
- So now we ask for our priority, etc. all over again. That suggests that the OpenBinder user-side code is treating all of the above as one single “transaction” (though I don’t want to overload that word) and now we’re starting another one.
- getpid() = 751
- getuid32() = 0
- getpriority(PRIO_PROCESS, 0) = 20
- And here is that next transaction. The target is 0×1, which I believe to be that reference to the service discovery object we asked for. We’re asking for opcode 2, which presumably means we want to get a reference to a named object. The body this time is simple - it’s just the PID, probable UID, and a string containing the name of the service (”phone” in Unicode).
- ioctl(3, BINDER_WRITE_READ, {write_size=40,write_consumed=40,write_buffer=0xa438,read_size=256,read_consumed=8,read_buffer=0xa328,write_data=[ bcTRANSACTION({target=0×00000001,cookie=0×00000000,code=0×00000002,flags=0×00000000, priority=80,data_size=24,offsets_size=0,data={buffer=0×0000a660,offsets=0×00000000,*buffer= [0xef,0×02,0×00,0×00,0×00,0×00,0×00,0×00,0×05,0×00,0×00,0×00,0×70,0×00,0×68,0×00,0×6f,0×00,0×6e,0×00,0×65,0×00,0×00,0×00, “…………p.h.o.n.e…”],*offsets=[”"]}})], read_data={[brNOOP,brTRANSACTION_COMPLETE]}) = 0
- And we wait for a reply. Again, we get one; it’s 24 bytes long and again contains one reference. I bet that reference is a reference to the phone service, and I bet it’s handle 0×02.
- ioctl(3, BINDER_WRITE_READ, {write_size=0,write_consumed=0,write_buffer=0xa438,read_size=256,read_consumed=44,read_buffer=0xa328,write_data=[],read_data= {[brNOOP,brREPLY({flags=0×00000000,priority=80,data_size=24,offsets_size=4,data={buffer=0×40008000,offsets=0×40008018, *buffer=[…]}})]}) = 0
- And the start of another ‘transaction’.
- getpid() = 751
- getuid32() = 0
- getpriority(PRIO_PROCESS, 0) = 20
- Now the really meaty bit where we actually make the request. We increase the reference count on that ‘phone’ object we got given, free the buffer, and then start a transaction to the phone object (target 0×2). The opcode is 2, which is what we specified on the command-line. The body contains purely the string we specified on the command-line: 123.
- ioctl(3, BINDER_WRITE_READ, {write_size=64,write_consumed=64,write_buffer=0xa438,read_size=256,read_consumed=8,read_buffer=0xa328, write_data=[bcINCREFS(target=0×00000002),bcACQUIRE(target=0×00000002), bcFREE_BUFFER(ptr=0×40008000),bcTRANSACTION({target=0×00000002,cookie=0×0000001b, code=0×00000002,flags=0×00000000,priority=80,data_size=20,offsets_size=0, data={buffer=0×0000a6e0,offsets=0×00000000,*buffer= [0xef,0×02,0×00,0×00,0×00,0×00,0×00,0×00,0×03,0×00,0×00,0×00,0×31,0×00,0×32,0×00,0×33,0×00,0×00,0×00, “…………1.2.3…”],*offsets=[”"]}})], read_data={[brNOOP,brTRANSACTION_COMPLETE]}) = 0
- Finally, we block to wait for a reply. We’re given 8 bytes containing no references to any objects… and I bet those bytes describe a NULL parcel.
- ioctl(3, BINDER_WRITE_READ, {write_size=0,write_consumed=0,write_buffer=0xa438,read_size=256,read_consumed=44,read_buffer=0xa328,write_data=[], read_data={[brNOOP,brREPLY({flags=0×00000000,priority=80,data_size=8,offsets_size=0,data={buffer=0×40008000,offsets=0×40008008, *buffer=[…]}})]}) = 0
- And we’re done.
- writev(1, [{”Result: Parcel(NULL)\n”, 21}], 1) = 21
- exit_group(0) = ?
Where to go next? I want to:
- Figure out how to get strace to ’see’ inside the mmap’ped /dev/binder, so I can see the replies coming back from the kernel (and thus ultimately from other processes
- Play around a bit to see if I can work out any more (e.g. the meaning of the cookies, or whether the second word of the transaction body really is the UID, or whether ints make a difference to the other data in the body)
- Use the knowledge gained from this to make a stab at understanding some of the (massive) logs gained from applying the same strace tools to runtime and Zygote and the things they spawn
- See if I can build the Binder shell and run it on Android. It’s like ’service’ only more sophisticated.
- Look more into the known, documented stuff about the AIDLs and the Service classes on the Java (Dalvik) side. I’d like to think the opcodes are a direct mapping into an AIDL. I’d like to try creating my own service and interacting with it. I haven’t really looked into the Java side stuff so I’ve no idea if it’s possible.
The odds of me getting around to any of the above any time soon are slim!

December 16th, 2007 at 2:35 am
Do you have any idea about undefined syscalls which are the last several lines
on “1. General process startup” ?
# syscall_983045(0xbeef9d3c, 0, 0×20, 0, 0xbeeda000, 0xbeef9df0, 0xbeef9e30, 0xf0005, 0xbeef9e30, 0, 0×8858, 0×8854, 0, 0xbeef9d30, 0xafe20a53, 0xafe0949c, 0×60000010, 0xbeef9d3c, 0, 0, 0, 0xc8c8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) = 0
# SYS_281(0×1, 0×1, 0, 0xafe40d20, 0xafe371bc) = 3
# SYS_283(0×3, 0xbeef9c9a, 0×13, 0, 0×3) = 0
# SYS_297(0×3, 0xbeef9cd4, 0, 0×1, 0xbeef9cd4) = 4
# mmap2(NULL, 32768, PROT_READ, MAP_SHARED, 4, 0) = 0×40000000
# close(4) = 0
# close(3) = 0
3 and 4 seem to be file descriptor created by undefined syscalls.
It mmap’ed with PROT_READ and MAP_SHARED. Is this some kind of
receive buffer from open binder driver??
December 16th, 2007 at 9:14 am
Hi, syscall983045 seems to be setting the thread local storage… but if that’s the case I don’t know why it would have so many arguments.
The SYS_281 etc. I don’t know! But this suggests they might be socket-related. Then again, I don’t know if these numbers will differ between ucLinux and the Android kernel.
December 16th, 2007 at 10:10 am
OK, I’ve looked into them in arch/arm/kernel/calls.S in the Android source code.
281 is ’socket’.
283 is ‘connect’.
297 is ‘recvmsg’.
Odd that they’re not understood by strace for ARM. Looks like that can be fixed by altering the relevant syscallent.h in the strace source code, though.
December 16th, 2007 at 1:55 pm
Thank you, Adrian.
Your other entries are interesting, too.