11

I wrote a very simple program with calls time() to illustrate the use ofstrace, but I'm having a problem; the time() call doesn't seem to actually produce a syscall!

I ended up stepping into the time() function in GDB and now I'm more confused than ever. From the disassembly of the time() function:

0x7ffff7ffad90 <time>:  push   rbp
0x7ffff7ffad91 <time+1>:    test   rdi,rdi
0x7ffff7ffad94 <time+4>:    mov    rax,QWORD PTR [rip+0xffffffffffffd30d]        # 0x7ffff7ff80a8
0x7ffff7ffad9b <time+11>:   mov    rbp,rsp
0x7ffff7ffad9e <time+14>:   je     0x7ffff7ffada3 <time+19>
0x7ffff7ffada0 <time+16>:   mov    QWORD PTR [rdi],rax
0x7ffff7ffada3 <time+19>:   pop    rbp
0x7ffff7ffada4 <time+20>:   ret 

How is this function actually getting the current time if it does not call the kernel? Its flow is:

  • Prologue
  • Get some value from (0x7ffff7ffad94 + 0xffffffffffffd30d) (0x7ffff7ff80a8) and put it in rax (to be returned)
  • Check if rdi (first argument) was null
  • If not put the value in rax (return value) there also
  • Epilogue

This makes sense with the functionality of time(); if the argument is null, it just returns the value, but if not it also puts it in the argument. The question I have is, where is it getting the time value? What's so magical about address 0x7ffff7ff80a8, and how does it do this without a syscall?

I'm using GCC 6.3.0 and Ubuntu GLIBC 2.24-9ubuntu2.2.

Leonora Tindall
  • 1,391
  • 2
  • 12
  • 30

1 Answers1

9

Read time(7). Probably your call to time(2) uses the vdso(7) (maybe via clock_gettime(2) or via __vdso_time). If vdso(7) is used,

When tracing systems calls with strace(1), symbols (system calls) that are exported by the vDSO will not appear in the trace output.

Details could be kernel and libc specific (and of course architecture specific).

For similar vDSO reasons, strace date don't show any time-related syscalls.

And vDSO is a really handy feature (subject to ASLR). Thanks to it, timing calls (e.g. clock_gettime(2)...) go really quick (about 40 nanoseconds on my i5-4690S). AFAIU, no context switch (or user to kernel mode transition) is happening.

So your 0x7ffff7ff80a8 is probably sitting in the vDSO (and the kernel ensures it contains the current time). You might check by using proc(5) (e.g. reading and showing /proc/self/maps from your program), or perhaps using ldd(1) and pmap(1)

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • 1
    That's right, no kernel/user transition. The VDSO pages are code + data that the kernel maps into the address-space of every process, at a fixed address. User-space (specifically glibc's wrappers for the some system calls like `time`, `getpid`, etc) simply runs a regular near-`call` instruction to call a function in the VDSO page. The reason the kernel provides user-space code for them is that they normally never need to enter the kernel at all, just read from the read-only page where the kernel exports scale factors for RDTSC, and the process's PID. – Peter Cordes Jan 14 '18 at 21:24
  • 1
    On x86 in 32-bit mode, the recommended way to make fast system calls is by calling into the VDSO: If `sysenter` is available, it has the code for the user-space side of the `sysenter` dance that provides the kernel with an EIP to return to, or it uses AMD 32-bit `syscall`, or it falls back to `int 0x80`. (The kernel chooses what to put in the VDSO page it exports based on boot-time CPU detection.) But in 64-bit x86 user-space, glibc uses `syscall` directly, because the `syscall` instruction is baseline for x86-64. – Peter Cordes Jan 14 '18 at 21:30
  • 1
    Oops, actually the VDSO pages can be ASLRed; the functions aren't at fixed addresses. The exported pages have ELF headers, so the runtime dynamic linker takes care of things, just like if the VDSO was a shared library mapped from a disk file. (The kernel writes the VDSO base address somewhere the dynamic linker can find it.) See https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/#virtual-system-calls. – Peter Cordes Jan 14 '18 at 21:34