ltrace.conf(5)

πŸ’– ltrace.conf β€” the config file for ltrace(1) πŸ’–

okay bestie, pull up a chair, we're doing ltrace.conf(5) 🫢


🌸 NAME 🌸

ltrace.conf β€” the configuration file for ltrace(1). it's giving "how do I tell ltrace what the function arguments actually mean". iykyk. ✨


✨ DESCRIPTION ✨

So this page is all about ltrace.conf, which is the file that describes prototypes of functions in binaries so that ltrace(1) can actually use them. ltrace literally needs this info to display function call arguments. without it, it's just standing there. πŸ’…

Each line of a config file describes at most a single item. Lines made entirely of white space get ignored (rude, but valid). Lines starting with a semicolon or a hash character are comment lines, and they get ignored too. πŸ‘‘

The things you can describe are either function prototypes, or definitions of type aliases.


πŸŽ€ PROTOTYPES πŸŽ€

A prototype describes the return type and the parameter types of a single function. The syntax goes like this:

[function] LENS NAME ([LENS{,LENS}]);

NAME is the (mangled) name of a symbol. In the elementary case, LENS is just... a type. Both lenses and types are described below, don't worry. 🌈

The function keyword, if it's present, has no effect. Like, zero. The only reason it exists is so you can force a line to be interpreted as a function prototype when the return type is a keyword that would otherwise start a different kind of line (such as import). it's a vibe-preservation keyword fr.

For example, a simple function prototype might look like this:

int kill(int,int);

And yes, despite the apparent similarity with C, ltrace.conf is really its own language that's only somewhat inspired by C. do not be fooled by the family resemblance bestie 😭


πŸ¦‹ TYPES πŸ¦‹

ltrace understands a whole range of primitive types. Those get interpreted according to C convention, native on a given architecture. Like, ulong is interpreted as a 4-byte unsigned integer on a 32-bit GNU/Linux machine, but an 8-byte unsigned integer on a 64-bit GNU/Linux machine. same word, different body, we love that for it ✨

  • void β€” denotes that a function does not return anything. Can also be used to construct a generic pointer, i.e. a pointer-sized number formatted in hexadecimal format.
  • char β€” 8-bit quantity rendered as a character.
  • ushort, short β€” denotes unsigned or signed short integer.
  • uint, int β€” denotes unsigned or signed integer.
  • ulong, long β€” denotes unsigned or signed long integer.
  • float β€” denotes floating point number with single precision.
  • double β€” denotes floating point number with double precision.

πŸ“ Composed types πŸ“

Besides primitive types, the following composed types are possible, and they're lowkey the fun ones:

  • struct([LENS{,LENS}]) β€” describes a structure with the given types as fields, e.g. struct(int,int,float).

Alignment is computed as customary on the architecture. Custom alignment (e.g. packed structs) and bit-fields are not supported. It's also not possible to differentiate between structs and non-POD C++ classes, for arches where it makes a difference. soz πŸ’€

  • array(LENS,EXPR) β€” describes an array of length EXPR, which is composed of types described by LENS, e.g. array(int, 6).

Note that in C, arrays in the role of a function argument decay into pointers. ltrace currently handles this automatically, but for full formal correctness, any such arguments should be described as pointers to arrays. it's a formality thing, but she's correct and she knows it πŸ’…

  • LENS* β€” describes a pointer to a given type, e.g. char* or int***. Note that the former example actually describes a pointer to a character, not a string. See below for the string lens, which is the one that applies to those cases. 🫢

πŸ’… LENSES πŸ’…

Lenses change the way that types are described. In the simplest case, a lens is directly a type. Otherwise, a type is decorated by the lens. ltrace understands the following lenses:

  • bin(TYPE) β€” the argument, which should be an integer type, is formatted in base-2.
  • oct(TYPE) β€” the argument, which should be an integer type, is formatted in base-8.
  • hex(TYPE) β€” the argument, which should be an integer or floating point type, is formatted in base-16. Floating point arguments are converted to double and then displayed using the %a fprintf modifier.
  • hide(TYPE) β€” the argument is not shown in the argument list. she's hidden, she's mysterious, she said no photos πŸ“Έ
  • bool(TYPE) β€” arguments with zero value are shown as "false", others are shown as "true". iconic binary behaviour honestly.
  • bitvec(TYPE) β€” the underlying argument is interpreted as a bit vector, and a summary of bits set in the vector is displayed. For example, if bits 3, 4, 5 and 7 of the bit vector are set, ltrace shows <3-5,7>. An empty bit vector is displayed as <>. If there are more bits set than unset, the inverse is shown instead: e.g. ~<0> when a number 0xfffffffe is displayed. A full set is thus displayed ~<>.

If the underlying type is integral, then bits are shown in their natural big-endian order, with LSB being bit 0. E.g. bitvec(ushort) with value 0x0102 would be displayed as <1,8>, irrespective of underlying byte order.

For other data types (notably structures and arrays), the underlying data is interpreted byte after byte. Bit 0 of the first byte has number 0, bit 0 of the second byte number 8, and so on. Thus bitvec(struct(int)) is endian sensitive, and will show the bytes comprising the integer in their memory order. Pointers are first dereferenced, thus bitvec(array(char, 32)*) is actually a pointer to a 256-bit bit vector. obsessed with the specificity ngl 🧚

  • string(TYPE), string[EXPR], string β€” the first form of the argument is canonical, the latter two are syntactic sugar. In the canonical form, the function argument is formatted as a string. The TYPE can have either of the following forms: X*, or array(X,EXPR), or array(X,EXPR)*. X is either char for normal strings, or an integer type for wide-character strings.

If an array is given, the length will typically be a zero expression (but doesn't have to be). Using an argument that is a plain array (i.e. not a pointer to array) makes sense e.g. in C structs, in cases like struct(string(array(char, 6))), which describes the C type struct {char s[6];}.

Because simple C-like strings are pretty common, there are two shorthand forms. The first shorthand form (with brackets) means the same as string(array(char, EXPR)*). Plain string without an argument is then taken to mean the same as string[zero].

Note that char* by itself describes a pointer to a char. ltrace will dereference the pointer, and read and display the single character that it points to. one (1) character. that's it. that's the whole thing. πŸ™ˆ
  • enum(NAME[=VALUE]{,NAME[=VALUE]})
  • enum[TYPE](NAME[=VALUE]{,NAME[=VALUE]}) β€” this describes an enumeration lens. If an argument has any of the given values, it is instead shown as the corresponding NAME. If a VALUE is omitted, the next consecutive value following after the previous VALUE is taken instead. If the first VALUE is omitted, it's 0 by default.

TYPE, if given, is the underlying type. It is thus possible to create enums over shorts or longs β€” arguments that are themselves plain, non-enum types in C, but whose values can be meaningfully described as enumerations. If omitted, TYPE is taken to be int. slay, flexible queen πŸ‘‘


⭐ TYPE ALIASES ⭐

A line in a config file can, instead of describing a prototype, create a type alias. Instead of writing the same enum or struct in many places (and possibly updating it when it changes), you can introduce a name for such a type, and later just use that name. no more copy-paste, we're healing 🫢

typedef NAME = LENS;

🌈 IMPORT DIRECTIVES 🌈

It's possible for config files to import definitions from other config files. A line of the form:

import "FILENAME";

will make all definitions from FILENAME.conf available in the current file. The imported file is searched for in the same directories as when looking up a config file corresponding to a library; see ltrace(1) for details. sharing is caring fr fr ✨


🧚 RECURSIVE STRUCTURES 🧚

ltrace allows you to express recursive structures. Such structures are expanded to the depth described by the parameter -A. To declare a recursive type, you first have to introduce the type to ltrace by using a forward declaration. Then you can use the type in other type definitions in the usual way:

typedef NAME = struct;
typedef NAME = struct(NAME can be used here)

For example, consider the following singly-linked structure and a function that takes such a list as an argument:

typedef int_list = struct;
typedef int_list = struct(int, int_list*);
void ll(int_list*);

Such declarations might lead to an output like the following:

ll({ 9, { 8, { 7, { 6, ... } } } }) = <void>

ltrace detects recursion and will not expand already-expanded structures. Thus a doubly-linked list would look like the following:

typedef int_list = struct;
typedef int_list = struct(int, int_list*, int_list*);

With output e.g. like:

ll({ 9, { 8, { 7, { 6, ..., ... }, recurse^ }, recurse^ }, nil })

The "recurse^" tokens mean that the given pointer points to a structure that was expanded in the previous layer. Simple "recurse" would mean that it points back to this object. E.g. "recurse^^^" means it points to a structure three layers up. For a doubly-linked list, the pointer to the previous element is of course the one that has been just expanded in the previous round, and therefore all of them are either recurse^, or nil. If the next and previous pointers are swapped, the output adjusts correspondingly:

ll({ 9, nil, { 8, recurse^, { 7, recurse^, { 6, ..., ... } } } })

the way that recurse^^^ walks back up the layers? main character energy honestly πŸ’œ


πŸŽ€ EXPRESSIONS πŸŽ€

ltrace has support for some elementary expressions. Each expression can be either of the following:

  • NUM β€” an integer number.
  • argNUM β€” value of NUM-th argument. The expression has the same value as the corresponding argument. arg1 refers to the first argument, arg0 to the return value of the given function.
  • retval β€” return value of the function, same as arg0.
  • eltNUM β€” value of NUM-th element of the surrounding structure type. E.g. struct(ulong,array(int,elt1)) describes a structure whose first element is a length, and second element an array of ints of that length. bestie said "self-describing struct" and she ATE πŸ”₯
  • zero
  • zero(EXPR) β€” describes an array which extends until the first element, whose each byte is 0. If an expression is given, that is the maximum length of the array. If a NUL terminator is not found earlier, that's where the array ends. πŸ’€

okay that's the first half, keep reading, there's more coming πŸ’–

✨ PARAMETER PACKS ✨

Okay bestie, sometimes a function's actual prototype is a little ✨fluid✨ depending on exactly which parameters you hand it. Like printf? The number and types of its parameters are literally not knowable in advance, but ltrace might be able to figure them out at runtime. This feature has wider applicability, but currently the only parameter pack ltrace supports is the printf-style format string itself:

  • format β€” when format shows up in the parameter list, ltrace parses the underlying string argument, and GNU-style format specifiers decide what the following actual arguments are. E.g. if the format string is "%s %d\n", it's as if the format got replaced by string, string, int. Slay.

πŸ’– RETURN ARGUMENTS πŸ’–

C functions often use one or more arguments to return values back to the caller. The caller provides a pointer to storage, and the called function initializes it. Ltrace has some support for this idiom, and it's giving main character energy.

When a traced binary hits a function call, ltrace first fetches all arguments. Then it displays the left portion of the argument list. Only when the function returns does ltrace display the right portion too. Typically, the left portion takes up all the arguments, and the right portion only contains the return value. But ltrace lets you configure exactly where to put the dividing line by using a + operator placed in front of an argument:

int asprintf(+string*, format);

Here, the first argument to asprintf is denoted as a return argument, which means displaying the whole argument list is delayed until the function returns:

a.out->asprintf( <unfinished ...>
libc.so.6->malloc(100)                   = 0x245b010
[... more calls here ...]
<... asprintf resumed> "X=1", "X=%d", 1) = 5
It is currently not possible to have an "inout" argument that passes information in both directions. So no bi-directional bestie behavior, sorry. 😭

🌈 EXAMPLES 🌈

In the following, the first is the C prototype, and following that is the ltrace configuration line. Periodt.

void func_charp_string(char str[]);

C prototype:

void func_charp_string(char str[]);

ltrace configuration line:

void func_charp_string(string);

enum e_foo {RED, GREEN, BLUE};

C prototype:

enum e_foo {RED, GREEN, BLUE};
void func_enum(enum e_foo bar);

ltrace configuration line:

void func_enum(enum(RED,GREEN,BLUE));
  • or -
typedef e_foo = enum(RED,GREEN,BLUE);
void func_enum(e_foo);

void func_arrayi(int arr[], int len);

C prototype:

void func_arrayi(int arr[], int len);

ltrace configuration line:

void func_arrayi(array(int,arg2)*,int);

struct S1 {float f; char a; char b;};

C prototypes:

struct S1 {float f; char a; char b;};
struct S2 {char str[6]; float f;};
struct S1 func_struct(int a, struct S2, double d);

ltrace configuration line:

struct(float,char,char) func_struct(int,
struct(string(array(char, 6)),float), double);

πŸ‘‘ AUTHOR πŸ‘‘

Petr Machata <pmachata@redhat.com> β€” absolute icon. πŸ’–


That's it bestie, you ate. Sending love from the ltrace.conf(5) page, October 2012 edition πŸ’…βœ¨πŸ¦‹