# Implicit Thread Local Storage tapset
# Copyright (C) 2020, 2021 Red Hat, Inc.
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
# Reference: "ELF Handling For Thread-Local Storage" by Ulrich Drepper

/*
 * TCB Pointer points to the thread control block.  dtv points to the DTV
 * array.  DTV[1]-DTV[N]  point to tls entries for module I from the link
 * map.  DTV[1] is the main executable.  The dtv val points to the tls
 * entry where the tls variable value is located.  The tls variable value
 * is also located a fixed offset from the tcbhead so if relocations are
 * simple enough a compiler can generate a direct load.  Complicated
 * relocations may result in a compiler calling a runtime routine
 * __tls_get_addr which uses the DTV. 
 * 
 * 			        TCB Pointer
 * 			        |
 * 			        V
 * TLS				struct tcbhead
 * +---------------------+	+----------------+ 
 * |OFFSET|OFFSET|OFFSET |      | void *tcb	 |
 * |  2   |  1   |  0    |      | dtv_t *dtv	 | --+
 * +---------------------+	+----------------+   |
 * 		   ^-------------------+	     |
 *          v----------------------------------------+
 *      +----------------------------+ | 
 *  DTV |length|gen|dtv[1]|dtv[2]|...| |
 *      +----------------------------+ |
 * 	    dtv[n] |		       |
 *      +---------------------+        |
 * 	|unsigned long counter|	       |
 * 	|struct dtv_pointer { |	       |
 * 	|  void *val	      |->------+
 * 	|  void *to_free}     |
 * 	+---------------------|
 */ 

global tls_debug = 0
private global tls_link_map_name
private global tls_module = 0

%{
# if defined(__DYNINST__)
# if defined(__i386) || defined(__x86_64)
# include <asm/prctl.h>
# include <sys/prctl.h>
# endif
# endif
%}

// Get the thread context pointer that points to the tls thread control block
@__private30 function get_thread_context_pointer (tls_thread_reg_val:long) %{
# ifdef __i386
{
  unsigned long gs;
# if defined(__KERNEL__)
  rdmsrl(MSR_FS_BASE, gs);
# elif defined(__DYNINST__)
  int arch_prctl(int code, unsigned long *addr);
  arch_prctl(ARCH_GET_FS, &gs);
# endif
  STAP_RETVALUE = gs;
}
# elif defined __x86_64
{
  unsigned long fs;
#if defined(__KERNEL__)
  rdmsrl(MSR_FS_BASE, fs);
# elif defined(__DYNINST__)
  int arch_prctl(int code, unsigned long *addr);
  arch_prctl(ARCH_GET_FS, &fs);
# endif
  STAP_RETVALUE = fs;
}
# elif defined __s390x__
{
  unsigned long tlsval;
  asm volatile ("ear %0,%%a0" : "=r" (tlsval));
  asm volatile ("sllg %0,%0,32" : "=r" (tlsval));
  asm volatile ("ear %0,%%a1" : "=r" (tlsval));
  STAP_RETVALUE = tlsval;
}
# elif defined __aarch64__
{
  unsigned long tlsval;
  asm("mrs %0,tpidr_el0" : "=r" (tlsval));
  STAP_RETVALUE = tlsval;
}
# elif defined __powerpc64__
{
  unsigned long tcb;
  asm("subi %0,%1,28688," : "=r" (tcb) : "r" (STAP_ARG_tls_thread_reg_val)); // -0x7010
  STAP_RETVALUE = tcb;
}
# endif
%}


// Get inode of followed link
// TODO Does not follow symbolic links, e.g. to follow /lib64/libc.so.6
//      The idea was to do 'inode = get_inode_from_path (l_name); name = inode_path(inode);'

@__private30 function get_inode_from_path:long (tls_module_name) %{
  struct inode *tls_inode;
  struct path tls_path;
  int code;
  code = kern_path(/* string */ STAP_ARG_tls_module_name, LOOKUP_FOLLOW, &tls_path);
  if (code == 0)
    STAP_RETVALUE = (int64_t)tls_path.dentry->d_inode;
  else
    STAP_RETVALUE = 0;
%}


// Return basename of a path

@__private30 function basename (l_name:string)
{
   for (lni = 0; lni < strlen (l_name); lni++) {
      if (stringat(l_name, lni) == 0x2f)  // '/'
	 slash = lni;
   } 
   if (slash > 0) {
      slash += 1;
      return substr (l_name, slash, strlen(l_name)-slash);
   }
   else
      return l_name;
}


// Setup the link_map module/l_name mapping

@__private30 function setup_link_map ()
{
  delete tls_link_map_name
%( arch == "x86_64" %?
  l_map_count = @var("_rtld_global","/usr/lib64/ld-linux-x86-64.so.2")->_dl_nns
  // TODO Do namespaces need to be supported? assume [0]
  l_map = @var("_rtld_global","/usr/lib64/ld-linux-x86-64.so.2")->_dl_ns[0]->_ns_loaded;
  if (tls_debug > 1)
    println ("l_map=", @var("_rtld_global","/usr/lib64/ld-linux-x86-64.so.2")->_dl_ns[0]->_ns_loaded$);
%)
%( arch == "powerpc" %?
  l_map_count = @var("_rtld_global","/usr/lib64/ld64.so.2")->_dl_nns
  l_map = @var("_rtld_global","/usr/lib64/ld64.so.2")->_dl_ns[0]->_ns_loaded;
  if (tls_debug > 1) 
    println ("l_map=", @var("_rtld_global","/usr/lib64/ld64.so.2")->_dl_ns[0]->_ns_loaded$);
%)
%( arch == "s390" %?
  l_map_count = @var("_rtld_global","/usr/lib/ld64.so.1")->_dl_nns
  l_map = @var("_rtld_global","/usr/lib/ld64.so.1")->_dl_ns[0]->_ns_loaded;
  if (tls_debug > 1) 
    println ("l_map=", @var("_rtld_global","/usr/lib/ld64.so.1")->_dl_ns[0]->_ns_loaded$);
%)
     
  while (l_map)
    {
     l_name = user_string(l_map->l_name);
     if (l_name == "") {
	 l_map = l_map->l_next
	 continue
     }
     l_addr = user_uint64(l_map->l_addr);
     l_tls_modid = l_map->l_tls_modid
     name_base = basename (l_name);
     if (l_tls_modid != 0) {
	tls_link_map_name[l_tls_modid] = name_base;
	if (tls_debug)
	   printf("link_map: %s l_addr=%#lx modid=%d\n", tls_link_map_name[l_tls_modid], l_addr, l_tls_modid);
       }
     l_map = l_map->l_next
    }
}


// Compare module names.  
// sonames, real sonames, and linker sonames should compare equal

@__private30 function regex_module_compare (stap_mod:string, map_mod:string)
{
   // Not an soname
   if (stap_mod !~ "^lib.*$") 
      return stap_mod == map_mod;

   // Most common case libMATCH-1.soMATCH-2
   // MATCH-1 is the soname minus lib prefix and .so* suffix
   if (stap_mod !~ "lib([-\._\+a-zA-Z0-9]+)\.so([\.0-9]*)")
      return 0;
   stap_lib = matched(1);

   if (map_mod !~ "lib([-\._\+a-zA-Z0-9]+)\.so([\.0-9]*)")
      return 0;
   map_lib = matched(1);
   if (stap_lib == map_lib)
     return 1;
   
   // The above will miss e.g. libc.so.6/libc-2.31.so
   // MATCH-1 is the soname minus lib prefix and -version.so suffix
   if (stap_mod !~ "lib([_\+a-zA-Z0-9]+)([-\.0-9]*)\.so([\.0-9]*)")
     return 0;
   stap_lib = matched(1);
   if (map_mod !~ "lib([_\+a-zA-Z0-9]+)([-\.0-9]*)\.so([\.0-9]*)")
     return 0;
   map_lib = matched(1);
   if (stap_lib == map_lib)
     return 1;
   else
     return 0;
}
      

// Find the module by matching the probe name to the link_map name

@__private30 function set_tls_module_by_name (tls_module:string)
{
  matched_proc = basename(tls_module);

  if (tls_debug)
    printf("Looking for %s in link_map\n", matched_proc);
  foreach (lm in tls_link_map_name) {
    if (lm==0) continue;
    if (regex_module_compare (matched_proc, tls_link_map_name[lm])) {
       if (tls_debug)
	 printf("Name Match: %s \n", tls_link_map_name[lm]);
       return lm;
    }
  }
  return 1;
}


/**
 * sfunction __push_tls_address - Handle implicit tls variable relocation
 * 
 * Description: Called for DW_OP_GNU_push_tls_address relocation from loc2stap.css::location_context::translate
 */

function __push_tls_address (tls_var_offset:long, tls_module:string) 
{
%( arch == "powerpc" %?
   // It seems the value of r13 cannot be dependably relied on in get_thread_context_pointer
   r13 = register("r13")
   %:
   r13 = 0
%)
  tcbhead = get_thread_context_pointer (r13);
  if (tls_debug > 1)
    printf ("tls_module=%s tcbhead=%#lx/%d\n", tls_module, tcbhead, tcbhead);
  setup_link_map ();
  module = set_tls_module_by_name (tls_module);
  if (module >= 0) 
    {
      // dtv[-1] is module count
      // dtv[0] is generation count
      // dtv[1] is module[0] info, the main executable
      // dtv[2..n] is module info for dependent shared objects
      dtv$pointer$val = @cast(tcbhead, "tcbhead", "<linux/stp_tls.h>")->dtv[module]->pointer->val;
      if (tls_debug > 1)
	printf ("dtv$pointer$val=%#lx, tls_var_offset=%#lx, module=%d\n", dtv$pointer$val, tls_var_offset, module);
      return dtv$pointer$val + tls_var_offset;
    }
  else
    return 0;
}
