GDB Guide Part 8 - Invoking Function Calls
GDB Guides Series:
1. Introduction
It’s been quite a while hasn’t it? I hope you have been doing well and taking care of yourself. I recently had some junior colleagues asking me how it is possible to directly invoke certain functions while using GDB. Thus, I thought it would be a great opportunity to share some useful tips.
We will be looking at how to invoke different functions of different return types using a simple example: gdb_invoke_function.c
Let’s get started!
2. Syntax
When invoking functions manually in GDB, you have full control over function calls just like in C. You have to explicitly cast the function return type when invoking any function call. Optionally, you can also cast a type to the argument.
To call a function, use:
call (return_type)function((arg_type)arg1, (arg_type)arg2, ...)
# Example:
# └── call (int)add((int)$arg1, (int)$arg2)
If the function returns a value and you want to store it in a GDB variable for later use:
set $ret_val = (return_type)function((arg_type)arg1, (arg_type)arg2, ...)
# Example:
# └── set $ret = (int)add((int)$arg1, (int)$arg2)
For cases where the function symbol is stripped, we can also directly invoke the function call through the known function address.
call (return_type)function_addr((arg_type)arg1, (arg_type)arg2, ...)
# Example:
# └── call (int)0x4004bc((int)$arg1, (int)$arg2)
3. Examples
This section of the guide will solely reference gdb_invoke_function.c.
3.1 No Arguments
For a start, let’s invoke the do_nothing() function which takes in no arguments and does not return anything.
(gdb) call (void)do_nothing()
3.2 One Argument
Next, we can invoke the hello(const char *name) function which takes in a string as its only argument.
(gdb) call (void)hello("Gerald")
3.3 Two Arguments
Similarly, we can do the same for invoking the add(int a, int b) function which takes in 2 integers and returns the sum.
(gdb) set $sum = (int)add(24, 10)
(gdb) p $sum
$1 = 34
3.4 Struct Arguments
GDB is not just limited to primitive types. You can also pass struct pointers into functions, even in stripped binaries or cases where you no longer have symbol/type information.
In our example, we want to invoke get_person_name(person_t *person), which returns a pointer to the name field of a person_t struct.
Since GDB doesn’t know about person_t as a type (especially in stripped binaries), we can manually construct the struct in memory by calculating offsets from static analysis or DWARF debug info.
Taking a look at the definition of the person_t struct, we can manually calculate the size of the struct, which ends up being 259 bytes.
typedef struct {
uint16_t id; // 2 Bytes
char name[256]; // 256 Bytes
uint8_t is_deceased; // 1 Byte
} person_t; /* Total: 259 Bytes */
Thus, we allocate 259 bytes in the heap and assign the return address to a variable to store the pointer.
(gdb) set $person = (uint64_t)malloc(259)
Next, we populate the struct fields one by one. Set the id field (offset 0x0):
(gdb) set *(uint16_t *)$person = 1
Set the name field (offset 0x2):
(gdb) call (void)strcpy((char *)($person + 0x2), "Gerald")
# OR
(gdb) call (int)snprintf((char *)($person + 0x2), 256, "%s", "Gerald")
Set the is_deceased field (offset 0x102):
(gdb) set *(uint8_t *)($person + 0x102) = 0
Once the struct is initialized, we can pass it to the function and print the returned name.
(gdb) call (char *)get_person_name((uint64_t *)$person)
$4 = 0x110b3ac2 "Gerald"
As a good practice, we will free up our struct in the heap after we are done.
(gdb) call free($person)
4. Conclusion
Manually invoking functions within GDB opens up an entirely new level of power during debugging and reverse engineering. Whether you’re verifying program behavior, modifying runtime values, or analyzing stripped binaries with no symbols, function calls allow for deep insight and control.
We saw how you can:
- Call functions just like you would in C.
- Assign return values to GDB variables.
- Pass struct pointers by manually crafting memory layout, even without debug symbols.
These techniques are especially useful in situations where:
- You’re dealing with stripped binaries.
- You need to simulate program flow or reconstruct missing behavior.
- You want to dynamically patch or inspect runtime behavior with minimal friction.
Understanding how to construct valid arguments and call functions correctly allows you to turn GDB from a passive observer into an active debugger. This knowledge can also serve as the foundation for scripting more powerful automation through Python GDB extensions or agents.
That’s all for today! Happy reversing. 🤓