Skip to main content

C Notes

Concepts​

  • RWTH Grundgebiete der Informatik 1
    • Gegenstand der Vorlesung ist die Einführung in Programmiertechniken, Datenstrukturen und Algorithmen anhand von C.
    • Grundlegende Programmelemente:
      • Skalare und zusammengesetzte Datentypen, Anweisungen, Kontrollfluß, Funktionen, Klassen, C-Programmstruktur und Programmierumgebung;
    • Programmanalyse:
      • Wachstumsordnungen, Komplexitätsklassen, best/worst case Analyse;
    • Lineare Datenstrukturen:
      • Listen, Stacks, Queues, Iteration und Rekursion;
    • Nichtlineare Datenstrukturen und Suchverfahren:
      • Bäume, Graphen, Suchbäume, Hashtabellen;
    • Algorithmenentwurf:
      • Sortierverfahren, Heuristiken, Greedy-Algorithmen, grundlegende Optimierungsverfahren

About C​

  • C is a procedural language, whereas C++ is an object-oriented language

Documentation​

  • standard library
    • On Unix-like systems, the authoritative documentation of the actually implemented API is provided in the form of man pages.
    • On most systems, man pages on standard library functions are in section 3 (e.g. man 3 malloc, man 3 puts, etc.)
    • Section 7 may contain some more generic pages on underlying concepts (e.g. man 7 math_error in Linux).

Build​

Source Code​

  • Source code is generally understood to mean programming statements that are created by a programmer with a text editor or a visual programming tool and then saved in a file. washington.edu

Object Code​

  • With the help of an adequate compiler, for example GCC, source code is translated into object code, first.

Binary files (ELF files)​

  • ELF = "Executable and Linkable Format"
  • see notes "Build" → "Binary vs Executable"
  • What is an ELF file
  • Eventually, the linker translates the object code into a binary file that links the object code with the referenced libraries.
  • This binary file contains the single instructions as machine code that are understood by the CPU, and are executed as soon the compiled program is run.
  • The binary file follows a specific structure, and one of the most common ones is named ELF that abbreviates Executable and Linkable Format.
  • ELF is widely used for
    • executable files
    • relocatable object files
    • shared libraries
    • core dumps
  • The specification does not clarify the filename extension for ELF files. In use is a variety of letter combinations, such as
    • .axf,
    • .bin,
    • .elf,
    • .o,
    • .prx,
    • .puff,
    • .ko,
    • .so, and
    • .mod,
    • or none.
  • man elf gives you a handy summary about the structure of an ELF file

ELF Header​

  • reading the ELF header:
  • The ELF header is 32 bytes long, and identifies the format of the file.
  • It starts with a sequence of four unique bytes that are 0x7F followed by 0x45, 0x4c, and 0x46 which translates into the three letters E, L, and F.
  • Among other values, the header also indicates
    • whether it is an ELF file for 32 or 64-bit format,
    • uses little or big endianness,
    • shows the ELF version
    • as well as for which operating system the file was compiled for in order to interoperate with the right application binary interface (ABI) and cpu instruction set.

Preprocessor​

include​

  • difference between #include <lib.h> and #include "lib.h"
    • What differs is the locations in which the preprocessor searches for the file to be included (stackoverflow)
    • phth test: This matters because if you write #include <lib.h> instead of #include "lib.h" the preprocessor will not find lib.h !!!

ifndef, Include Guards​

From stackoverflow:

  • Those are called include guards.
  • Once the header is included, it checks if a unique value (in this case HEADERFILE_H) is defined. Then if it's not defined, it defines it and continues to the rest of the page.
  • When the code is included again, the first ifndef fails, resulting in a blank file.
  • That prevents double declaration of any identifiers such as types, enums and static variables.
  • Also it prevents recursive inclusions... Imagine alice.h includes bob.h and bob.h includes alice.h and they don't have include guards... - Kevin Dungs

Simple Example: Example

Anschaulich:

  • #ifndef und #endif umschließen eine Art IF Block, der nur ausgeführt wird, wenn die condition ndef (not defined) TRUE ist.
  • Beim ersten include des header files wird über das #define SOME_MAKRO ein Makro definiert.
  • Beim zweiten include desselben files ist #ifndef SOME_MAKRO FALSE und der "IF Block" wird geskipped.

Operator Precedence, Associativity​

  • When multiple operators of the same precedence level are used in an expression, they are evaluated according to their associativity (source)

Variables​

Initialization​

see Different ways to initialize a variable in C/C++

Global variables (aka External variables)​

  • external variable: a variable defined outside of any function
  • scope: the scope is the entire source code file that contains the variable definition
  • If you don't explicitly initialize an external variable when it's defined, the compiler initializes it to 0.
    • this is different than local variables!

Best practice​

  • use them rarely
    • because usage violates the principle of modular independence
      • i.e each function should be self-contained/modular
  • use them only when all or most of the program's functions need access to the variable
  • When a function uses an external variable, it is good programming practice to declare the variable within the function using the extern keyword (C: see Storage-class specifiers; C++: see Storage-class specifiers)
    • "You normally use extern when you want to refer to something that is not in the current translation unit, such as a variable that's defined in a library you will be linking to." (stackoverflow)

Local variables​

  • aren't automatically initialized to 0 by the compiler
    • if not initialized they have an undefined or garbage value

Static variables vs. Automatic variables​

  • local variables are automatic by default, i.e.
    • created anew each time the function is called
    • destroyed when execution leaves the function
      • thus doesn't retain its value between calls to the function in which it is defined.
  • In order for a local variable to retain its value between function calls, it must be defined as static with the static keyword.
    void print(int x)
    {
    static int lineCount;
    /* Additional code goes here */
    }
  • (optional) if you want to, you can include the auto keyword in the definition
    void func1(int y)
    {
    auto int count;
    /* Additional code goes here */
    }
  • static and auto are Storage-class specifiers (see below)

Storage Duration​

From cppreference ("C language" part): Storage duration

  • Storage-class specifiers: Specify storage duration (...) of objects and functions.
    • auto (automatic storage duration)
    • register (automatic storage duration)
    • static (static storage duration)
    • extern (static storage duration)
    • _Thread_local (thread storage duration)
  • Storage duration: Every object has a property called storage duration, which limits the object lifetime. There are four kinds of storage duration in C:
    • static
      • The storage duration is the entire execution of the program
      • All objects declared static have this storage duration.
    • automatic
      • The storage is allocated when the block in which the object was declared is entered and deallocated when it is exited by any means (goto, return, reaching the end).
      • All function parameters and non-static block-scope objects have this storage duration, as well as compound literals used at block scope.
    • thread
      • The storage duration is the entire execution of the thread in which it was created
    • allocated

Int​

  • don't convert int to float
    • when an integer type is converted to a floating-point type, the resulting floating-point value might not exactly match the integer value (e.g. 2.999995 instead of 3)
    • keep integer values in type short, int, long, or variables long long

Low-order byte and High-order byte​

From stackoverflow:

  • In C, the size of an int is implementation defined, but is usually 2, or 4 bytes in size. The high-order byte would be the byte that contains the largest portion of the value. The low-order byte would be the byte that contains the smallest portion of the value.
    • For example, if you have a 16-bit int, and the value is 5,243, you'd write that in hex as 0x147B (phth: 4 hex Stellen, weil 16-bit int).
      • The high order byte is the 0x14 (phth: 1 byte = 8 bit, sodass 256 Werte möglich sind, was in hex 2 Ziffern sind! Daher die 14 und nicht nur die 1 hinter dem 0x.), and
      • the low-order byte is the 0x7B.
    • A char is only 1 byte, so it is always contained within the lowest order byte. When written in hex (in left-to-right fashion)
      • the low-order byte will always be the right-most 2 digits, and
      • the high-order byte will be the left-most 2 digits (assuming they write all the bytes out, including leading 0s).

Arrays​

Definition​

  • array: a collection of data storage locations, each storing the same type of data and having the same name
  • array elements: the storage locations of an array

Declaration​

  • When you declare an array, the compiler sets aside a block of memory large enough to hold the entire array.
  • Individual array elements are stored in sequential memory locations.

Initialization​

  • You can initialize all or part of an array when you first declare it
    • ANSI standard: the elements that are not initialized will be set to zero.
      • however, it is best to make sure you know what a value is initialized to by setting it yourself.
  • If you omit the array size, the compiler creates an array just large enough
  • If you include too many initializers, the compiler detects an error.
    • Note: This is different from using subscripts which are out of bounds which will not be detected by the compiler!

Subscripts​

  • the last element is one less than the number of elements in the array.

Passing arrays to a function​

  • problem 1: The only way you can pass an array to a function is by using a pointer to an array. (see Passing Function arguments)
  • problem 2: function that can handle arrays of different sizes (e.g. a function that finds the largest element in an array of integers)
    • How does the function know the size of the array whose address it was passed?
      • method 1: identify the last array element by storing a special value there (see Strings)
      • method 2: Pass the function the array size as an argument

Copy 2d array and assign it to another​

From stackoverflow:

char arr[3][3] = { {'a','b','c'},{'d','e','f'},{'g','h','i'} };
char arr_copy[3][3];

memcpy(arr_copy, arr, sizeof arr_copy);

Out of bounds error​

  • The C compiler doesn't recognize whether your program uses an array subscript that is out of bounds.
  • Your program compiles and links, but out-of-range subscripts generally produce erroneous results
    • from source:
      • though, clang will do it for some cases if you pass -fcatch-undefined-behavior
      • The gcc equivalent is -fsanitize=undefined. The -ggdb flag also helps; it causes compilation with extra debugging info, which can be the difference between having/not having line numbers pointing to the error in the code in fsanitizer output.
      • but this does not mean it is part of the C Standard!

Sometimes Dynamically Allocated Integer Arrays Default Initialize to 0​

Best practice​

  • use #define statements to create constants that can be used when declaring arrays. Then you can easily change the number of elements in the array.
  • do not use multidimensional arrays with more than three dimensions

Functions​

Return statements​

  • in the main function a return 0; is optional in C99, i.e. return 0; will be returned by default if there is no explicit return statement at the end of the function stackoverflow

Inline Functions​

  • from What is the use of the inline keyword in C?
    • A C code can be optimized in two ways: For Code size and for Execution Time.
    • So, it tells the compiler to build the function into the code where it is used with the intention of improving execution time.
    • If you declare small functions like setting/clearing a flag or some bit toggle which are performed repeatedly, inline, it can make a big performance difference with respect to time, but at the cost of code size.

Best practice​

Program Input/Output, Devices​

  • input
    • Data moved from an external location into RAM, where the program can access it
  • output
    • Data sent to a location external to the program (common destinations: screen, printer, disk files)
  • devices
    • collective term for input sources and output destinations

Streams​

  • C carries out all input and output operations by means of streams
  • stream: a sequence of bytes of data (e.g. a sequence of characters)
    • input stream: sequence of bytes flowing into a program
    • output stream: sequence of bytes flowing out of a program
  • advantage: streams make input/output programming device independent
  • file: every C stream is connected to a file, which is an intermediate step between the stream that your program deals with and the actual physical device being used for input or output
    • the details of interactions between streams, files, and devices are taken care of automatically by the C library functions and the operating system
  • 2 modes of C streams
    • text stream: consists only of characters (e.g. text data)
    • binary stream: consists of any sort of data

Five Standard Streams​

(the first 3 are C ANSI standard, the last 2 are only under Windows and DOS)

Format SpecifierTypeDevice
stdinStandard inputKeyboard
stdoutStandard outputScreen
stderrStandard errorScreen
stdprn*Standard printerPrinter (LPT1:)
stdaux*Standard auxiliarySerial port (COM1:)

Stream Functions​

  • functions that deal with stream input and output

Input Functions​

  • hierarchy of 3 levels
    • character input (getchar, getch, getche, getc, fgetc, ungetc)
    • line input (gets, fgets)
    • formatted input (scanf, fflush)

Output Functions​

  • 3 categories
    • character output (putchar, putc, fputc)
    • line output (puts, fputs)
    • formatted output (printf, fprintf)

printf​

Note: Programmers often add print statements during debugging. Such statements should always flush the stream. Otherwise, if the program crashes, output may be left in the buffer, leading to incorrect inferences about where the program crashed.

Understanding the printf Declaration​

  • prototype: int printf(const char *restrict format, ...);
    • Ellipsis in the declaration
      • the ellipsis notation in the declaration of printf() means that default argument promotions occur, but that affects only the float type and integer types smaller than int (short, char, etc).
      • from What is ellipsis operator in c:
        • An ellipsis is used to represent a variable number of parameters to a function. For example void format(const char* fmt, ...) in C could then be called with different types and numbers of parameters such as format("%d-%d-%d", 2010, 9, 25); and format("product: %s, price: %f", "HDD", 450.90);.

Escape sequences​

Conversion command​

%[flag][field_width][.[precision]][l]conversion_char

Components of the command in brackets are optional.

Format specifiers​

Format SpecifierType
%cCharacter
%dSigned integer
%e or %EScientific notation of floats
%fFloat values
%g or %GSimilar as %e or %E
%hiSigned integer (short)
%huUnsigned Integer (short)
%iUnsigned integer
%l or %ld or %liLong
%lfDouble
%LfLong double
%luUnsigned int or unsigned long
%lli or %lldLong long
%lluUnsigned long long
%oOctal representation
%x or %XHexadecimal representation
%pPointer
%sString
%uUnsigned int
%nPrints nothing
%%Prints % character

These are the basic format specifiers. We can add some other parts with the format specifiers. These are like below

  • A minus symbol (-) sign tells left alignment
  • A number after % specifies the minimum field width. If string is less than the width, it will be filled with spaces
  • A period (.) is used to separate field width and precision

Precision​

From C99 §7.19.6.1:

  • A double argument representing a floating-point number is converted to decimal notation in the style [−]ddd.ddd, where the number of digits after the decimal-point character is equal to the precision specification.
  • If the precision is missing, it is taken as 6; if the precision is zero and the # flag is not specified, no decimal-point character appears. If a decimal-point character appears, at least one digit appears before it.
  • The value is rounded to the appropriate number of digits.

Field width​

Determines the minimum number of characters output:

  • decimal integer not starting with 0: output is padded on the left with spaces
  • decimal integer starting with 0: output is padded on the left with zeros
  • the * character: the value of the next argument of printf() (which must be an int) is used as field width, e.g. in printf("%*d", w, a); the integer w specifies the field width
  • no field width or field width narrower than output: field width just as wide as needed

Flag​

  • +: display signed numbers with leading + or -

fprintf​

  • same as printf except that printf always uses the stdout, whereas fprintf specifies the output stream
  • generally, used for output to disk files

Usage​

  • disk files

  • fprintf(stderr, "An error has occurred."); or for better maintainability:

    error_message("An error has occurred.");

    void error_message(char *msg)
    {
    fprintf(stderr, msg);
    }

    this way, only void error_message() needs to be changed in order to direct the error messages to some other destination.

Redirection​

  • redirect output: ./program > output.txt redirects stdout of ./program to output.txt (overwrites output.txt if it exists already!)

  • redirect input: ./program < input.txt (source code for ./program see below) will take input.txt as input rather than asking the user for input via keyboard

    /* program.c */

    #include <stdio.h>

    int main( void )
    {
    char buf[80];

    gets(buf);
    printf("The input was: %s\n", buf);
    return 0;
    }
  • >>: redirects stdout to a file and appends it to the file (instead of overwriting the file like >)

scanf, gets, fgets​

Read all characters up to the next newline character ('\n', must use single quotes else it is a string literal).

Control Characters​

  • EOF: If you're typing at the terminal and you want to provoke an end-of-file, use CTRL-D (unix-style systems) or CTRL-Z (Windows).

scanf​

  • limitations:
    • scanf() cannot take strings with spaces in between characters as input, gets() and fgets() can

gets​

  • prototype: char *gets(char *str);
  • input stream is read from stdin until
  • the newline ('\n') is replaced with a null character ('\0')
  • return value:
    • a pointer to the string (the same as str)
    • if gets() encounters an error or reads end-of-file: a null pointer
  • the string is stored at the location indicated by str
  • important: allocate sufficient memory space to store the string before calling gets()

fgets​

  • prototype: char *fgets(char *str, int n, FILE *fp);
    • FILE *fp is used to specify the input stream (e.g. stdin)
    • str indicates where the input string is stored
    • n specifies the maximum number of characters to be input
  • input stream is read until
    • a newline ('\n') or
    • end-of-line is encountered or
    • n - 1 characters have been read
  • the newline ('\n') is included in the string (different from gets()!) and a '\0' is appended before it is stored (i.e. strings are stored with trailing '\n\0')
  • the return values are the same as for gets()
  • the string is stored at the location indicated by str

Char​

  • use single quotations when initializing a variable
  • char constants:
    • literal:
      • enclose a single character in single quotation marks
        char code = 'x';
    • symbolic:
      • using #define
        #define EX 'x'
        char code = EX; /* Sets code equal to 'x' */
      • using const
        const char A = 'Z';
  • When you store the character a in a type char variable, the compiler is really storing the ASCII value 97
  • if you want to print the extended ASCII (aka CP437) characters, you must use an unsigned char

Unicode vs UTF-8​

Important for:

  • in NLP:
    • BPE (note from GPT-2 paper: "Despite its name, reference BPE implementations often operate on Unicode code points and not byte sequences.")
    • byte-level BPE

Remember:

from (Wiki: Unicode):

  • defines as of the current version (Unicode v15.0) 149,186 characters
  • Unicode can be stored using several different encodings, which translate the character codes into sequences of bytes (Recall: 1 byte = 2 Hex characters).
  • The Unicode standard defines three and several other encodings exist, all in practice variable-length encodings.
  • The most common encodings are the ASCII-compatible UTF-8, (...).

from (Wiki: UTF-8):

  • Extension of ASCII
  • UTF-8 is capable of encoding all 1,112,064 valid character code points in Unicode using one to four one-byte (8-bit) code units.
  • Code points with lower numerical values, which tend to occur more frequently, are encoded using fewer bytes.
  • It was designed for backward compatibility with ASCII: the first 128 characters of Unicode, which correspond one-to-one with ASCII, are encoded using a single byte with the same binary value as ASCII, so that valid ASCII text is valid UTF-8-encoded Unicode as well.

Strings​

Definitions​

  • a string is a sequence of characters delineated by
    • a pointer that points to the string's first character and
    • a null character \0 that indicates the end of the string ("null-terminated" string)
  • From C18 6.4.5:
    • A character string literal is a sequence of zero or more multibyte characters enclosed in double-quotes, as in "xyz".
    • A UTF–8 string literal is the same, except prefixed by u8.
    • A wide string literal is the same, except prefixed by the letter L,u, or U.

String Literals​

String Literals sind "besondere" char arrays, die im read-only memory sein können, aber nicht müssen (abhängig von C implementation). Nach ISO C dürfen sie nicht modifiziert werden!

From cppreference:

  • character string literal: The type of the literal is char[N], where N is the size of the string in code units of the execution narrow encoding, including the null terminator.
  • A string literal is not necessarily a string; if a string literal has embedded null characters, it represents an array which contains more than one string:
    char* p = "abc\0def"; // strlen(p) == 3, but the array has size 8
  • String literals are not modifiable (and in fact may be placed in read-only memory such as .rodata). If a program attempts to modify the static array formed by a string literal, the behavior is undefined.
    char* p = "Hello";
    p[1] = 'M'; // Undefined behavior
    char a[] = "Hello";
    a[1] = 'M'; // OK: a is not a string literal
    phth note: The latter variant is treated as
    char a[] = { 'H', 'e', 'l', 'l', 'o', '\0'};
    a[1] = 'M'; // OK: a is not a string literal
    thus, char a[] = "Hello"; copies each char byte of the string literal "Hello" into the newly created memory block of the array a, but the memory block storing "Hello"is not modified! a is a standard array, thus, like all standard arrays, it is modifiable. a points to the array's first element 'H' and not to the string literal "Hello"'s first element!
    • from cppreference:
      • Successive bytes of the string literal (...), including the terminating null byte/character, initialize the elements of the array

Notes:

  • phth: .rodata is the "read-only data segment", a portion of an object file or the corresponding address space of a program that contains static constants.
  • phth: "may be placed in read-only memory" means they do not have to!

From the C89 Rationale, 3.1.4 String literals:

  • String literals are specified to be unmodifiable.
  • This specification allows implementations (phth: z.B. gcc, clang, etc)
    • to share copies of strings with identical text,
    • to place string literals in read-only memory, and
      • d.h. ob string literals in read-only memory sind oder nicht ist abhängig von der implementation
    • perform certain optimizations.
  • However, string literals do not have the type array of const char,
    • in order to avoid the problems of pointer type checking, particularly with library functions, since assigning a pointer-to-const char to a plain pointer-to-char is not valid.
  • Those members of the Committee who insisted that string literals should be modifiable were content to have this practice designated a common extension (see F.5.5).

FAQ - string literals​

Good example code:

Create Strings as char arrays​

element-by-element:

char string[10] = { 'A', 'l', 'a', 'b', 'a', 'm', 'a', '\0' };

using string literal:

char string[10] = "Alabama";
char message[] = "Great Caesar\'s Ghost!";
  • use double quotes
  • The array's name is a type char pointer to the beginning of the string
  • the compiler calculates the size of the array for you, if no subscript is provided
    • Note: The expression within the brackets [] is referred to as a subscript
  • the compiler automatically adds the terminating null character at the end of the string

Create Strings without arrays​

Allocate string space at compilation​

static memory allocation: explicitly allocating memory in the program source code by declaring variables, structures, and arrays

char *message = "Great Caesar\'s Ghost!";

i.e. *message and message[] are equivalent!

Note:

  • The pointer *message itself is stored at a different location than the literal string "Great Caesar\'s Ghost!" (see section Pointer).
  • The array's name is a type char pointer to the beginning of the string

Allocate string space at runtime (malloc)​

dynamic memory allocation:

##include <stdlib.h>
##include <stdio.h>
int main( void )
{
/* allocate memory for a 100-character string */
char *str;
str = (char *) malloc(100 * sizeof(char));
if (str == NULL)
{
printf( "Not enough memory to allocate buffer\n");
exit(1);
}
printf( "String was allocated!\n" );
return 0;
}

Note that void *malloc() is typecast so that its return value is the proper type - a pointer to a char variable. Further details about malloc() are under section Memory.

Best Practices​

  • it's a good habit to use "\t" instead of a real tab
    • why?
      • in vim when using ":retab" on a program, it may change white space inside a string constant (see vim doc)

Pointer​

  • The address of a variable is actually the address of the first (lowest) byte it occupies
  • Each pointer is equal to the address of the first byte of the pointed-to variable

Size of pointers​

  • The size of a pointer in C/C++ depends upon different issues like Operating system, CPU architecture etc.
  • Usually it depends upon the word size of underlying processor, for example
    • for a 32 bit computer the pointer size can be 4 bytes
    • for a 64 bit computer the pointer size can be 8 bytes.
  • So, for a specific architecture pointer size will be fixed.
  • It is common to all data types like int *, float * etc.

Data type of pointers​

From stackoverflow:

  • But really, what is a type? The gist of it is that a type is a way to determine how data in memory is supposed to be interpreted. A variable of the type char represents a character. A variable of the type int represents an integer. Same applies to pointers: char *x is not of the type char, it's of the type char * (pointer to char). This means that char *x itself is a location in memory we can use to read one or more char values.
  • TL;TR: Yes, a pointer is a data type (void * in its purest form). The pure form is quite unusable (because you can't dereference it). Instead of having to cast the pointer every time you decide to use it, C offers the convenience of derived pointer types (like char *, int * and so on). But really, they're pointers, and therefore a data-type in their own right.

Where is a pointer stored?​

  • a pointer can be stored anywhere in the process' virtual memory (aka address space) which is typically organized in 6 sections:
    • environment section - used to store environment variables and command line arguments;
    • the stack, used to store memory for function arguments, return values, and automatic variables;
    • the heap (free store) used for dynamic allocation,
    • data section for uninitialized static and global variables
    • data section for initialized static and global variables
    • a text section where the actual code is kept
  • more precisely: from cprogramming.com:
    • The size of the virtual memory is the maximum size of the maximum size your computer can address using pointers (usually on a 32-bit processor each process can address 4 GB of memory).
    • Typically, in each process, the virtual memory available to that process is called its address space.
    • Even though every application has its own 4 GB (on 32-bit systems) of virtual memory, that does not necessarily mean that your program can actually use all of that memory.
      • On Windows, the upper 2 GB of that memory are allocated to the operating system kernel, and are unavailable to the process.
        • (Therefore, any pointer starting with 0x8xxxxxxx is unavailable in user space.)
      • On Linux, the upper 1 GB is kernel address space
  • stackoverflow:
    • a pointer can be created in any of these storage classes; and objects being pointed to can also be in any of these storage classes
    • Roughly, dynamic corresponds to "heap", and automatic corresponds to "stack".
      • e.g. malloc() dynamically allocates memory from the heap

void pointer​

In C++​

source

A void pointer can point to objects of any data type:

int nValue;
float fValue;

struct Something
{
int n;
float f;
};

Something sValue;

void* ptr;
ptr = &nValue; // valid
ptr = &fValue; // valid
ptr = &sValue; // valid

However, because the void pointer does not know what type of object it is pointing to, dereferencing a void pointer is illegal. Instead, the void pointer must first be cast to another pointer type before the dereference can be performed.

int value{ 5 };
void* voidPtr{ &value };

// std::cout << *voidPtr << '\n'; // illegal: dereference of void pointer

int* intPtr{ static_cast<int*>(voidPtr) }; // however, if we cast our void pointer to an int pointer...

std::cout << *intPtr << '\n'; // then we can dereference the result

NULL pointer​

Definition:

##define NULL ( (void *) 0)

C11 standard definition:

  • An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
  • If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

Usage​

  1. To initialize a pointer variable when that pointer variable hasn't been assigned any valid memory address yet.
int * pInt = NULL;
  1. To check for a null pointer before accessing any pointer variable. By doing so, we can perform error handling in pointer related code, e.g., dereference a pointer variable only if it's not NULL.
if(pInt != NULL) /* We could use if(pInt) as well */
{
/* Some code */
}
else
{
/* Some code */
}
  1. To pass a null pointer to a function argument when we don't want to pass any valid memory address.
int fun(int *ptr)
{
/*Fun specific stuff is done with ptr here*/
return 10;
}
fun(NULL);

Difference between a void pointer and a NULL pointer?​

From stackoverflow:

In C, there is void, void pointer and NULL pointer.

  • void is absence of type. I.E. a function returning a void type is a function that returns nothing.
  • void pointer: is a pointer to a memory location whose type can be anything: a structure, an int, a float, you name it.
  • A NULL pointer is a pointer to location 0x00, that is, no location. Pointing to nothing.

Examples:

void function:

void printHello()
{
printf("Hello");
}

void pointer:

void *malloc(size_t si)
{
// malloc is a function that could return a pointer to anything
}

NULL pointer:

char *s = NULL;
// s pointer points to nowhere (nothing)

Pointer arithmetic​

If you have a pointer ptr that points to a data object of type obj,

ptr++

has the same effect as

ptr += sizeof(obj);
// https://stackoverflow.com/a/8208106
// given:
int *p;
int a = 100;
p = &a;

// then:
ptr++; // Pointer moves to the next int position (as if it was an array)
++ptr; // Pointer moves to the next int position (as if it was an array)
++*ptr; // The value pointed at by ptr is incremented
++(*ptr); // The value pointed at by ptr is incremented
++*(ptr); // The value pointed at by ptr is incremented
*ptr++; // Pointer moves to the next int position (as if it was an array). But returns the old content
(*ptr)++; // The value pointed at by ptr is incremented
*(ptr)++; // Pointer moves to the next int position (as if it was an array). But returns the old content
*++ptr; // Pointer moves to the next int position, and then get's accessed, with your code, segfault
*(++ptr); // Pointer moves to the next int position, and then get's accessed, with your code, segfault

Address of a pointer / multiple indirection​

  • multiple indirection: You can use the address-of operator to find the address of a pointer, so you can have pointers to pointers.
    • The most common use involves arrays of pointers.
      • The most common use of an array of pointers is with strings.

Passing Function arguments​

  • Using "passing by reference" (coll.) we can overcome the following two limitations of functions:
    • Arguments are passed by value, and
    • the return statement can return only one value

Structs​

3 ways to access a structure member​

  • Using the structure name
  • Using a pointer to the structure with the indirection operator (*)
  • Using a pointer to the structure with the indirect membership operator (->)
    • sometimes called "structure pointer operator"

Therefore, if p_str is a pointer to the structure str,

str.memb
(*p_str).memb
p_str->memb

are equivalent.

typedef​

Whether you use typedef or a structure tag to declare structures makes little difference

typedef struct {
int x;
int y;
} coord;

or using a structure tag

struct coord {
int x;
int y;
};

Type Conversions​

Type Promotion (implicit conversion, automatic conversion)​

If the components of an expression have different types, an expression has the same type as its most comprehensive component. From least-comprehensive to most-comprehensive, the numerical data types are

  • char
  • short
  • int
  • long
  • long long
  • float
  • double
  • long double

Note: Promotion means that a copy of the variable is created and used in the expression evaluation. The type of the variable is not changed!

Promotion by Assignment: expression on the right side of an assignment statement is always promoted to the type of the data object on the left side of the assignment operator

  • may cause demotion
    • e.g. float to int (Caution: fractional part is lost!)

Cast / Typecast (explicit conversion)​

  • e.g. (float)i when i is an integer
  • can be performed on arithmetic expressions and pointers
  • makes an internal copy of the variable using the new type (similar to promotion)
  • most common use: f1 = (float)i1/i2; to avoid using the fractional part in an integer division
    • casting one of the int variables is sufficient
  • casting pointers:
    • Before you can use a void pointer, you must cast it to the proper type
    • don't need to cast any pointer
      • in order to assign a value to it
      • to compare it with NULL
    • must cast a pointer
      • before dereferencing
      • before performing pointer arithmetic with it
  • Casting a variable expression to void to suppress this ("unused local variables or function parameters") warning has become an idiom in the C and later C++ community, stackoverflow
    • The Standard says in §5.2.9/4 says: "Any expression can be explicitly converted to type cv void. The expression value is discarded.", stackoverflow

Memory​

Monitoring of memory​

Read How do I see how much free ram I really have? first! Also read section "When should I start to worry?"!

watch free -m

Bad signs​

dmesg -T | grep oom-killer
## same as
dmesg --ctime | grep oom-killer

should not show any output! If it does, it is a bad sign!

  • the -T (same as --ctime) flag is used to print human-readable timestamps
    • Be aware that the timestamp could be inaccurate! (see man dmesg)

Areas of memory​

When a program begins the operating system sets up following areas of memory (depending on the requirements of the compiler):

  • registers,
  • code space,
  • stack,
  • global namespace, and
  • free store (heap)

Registers​

  • area of memory built right into the CPU
  • important registers:
    • instruction pointer
      • set of registers responsible for pointing, at any given moment, to the next line of code
    • stack pointer (see section "stack")
    • frame pointer (see section "stack")

Code space​

  • memory set aside to hold the binary form of the instructions you created in your program

Stack​

  • area of memory allocated for your program to hold the data required by each of the functions in your program
  • last-in, first-out
    • data is pushed onto the stack and popped off the stack
  • setrlimit(): use RLIMIT_STACK (see "2022-09-22-notes-OS.md" → "resource-limits") to limit the stack size
    • If you exceed the RLIMIT_STACK limit (call stack too deep, or allocate too many variables on the stack), your program will receive a SIGSEGV.
  • stack pointer
    • analogy: series of cubbyholes
      • the top of the stack: whatever cubby the stack pointer (which is another register) happens to be pointing to
      • sequential address: Each of the cubbies has a sequential address
      • one of those sequential addresses is kept in the stack pointer register (i.e. the top of the stack)
      • Everything below that magic address, known as the top of the stack, is considered to be on the stack.
      • Everything above the top of the stack is considered to be off the stack and invalid.
    • when data is popped off the stack the stack pointer is moved down the stack
      • the data above the stack pointer is called garbage and is not reliable (it might or might not be changed at any time!)
  • Stack trace
Call Stack vs Stack (Abstract Data Type)​
  • the "stack" discussed here is not to be confused with the ADT called "stack"
  • Stack trace
    • "Stack also refers to a programming construct, thus to differentiate it, this stack is referred to as the program's function call stack."
      • Stack (abstract data type):
        • a stack is an abstract data type that serves as a collection of elements with two main operations:
          • Push, which adds an element to the collection, and
          • Pop, which removes the most recently added element.
        • Additionally, a peek operation can, without modifying the stack, return the value of the last element added.
        • The name stack is an analogy to a set of physical items stacked one atop another, such as a stack of plates.
        • A stack can be easily implemented either through an array or a linked list, as it is merely a special case of a list. In either case, what identifies the data structure as a stack is not the implementation but the interface: the user is only allowed to pop or push items onto the array or linked list, with few other helper operations.
        • data structure vs abstract data type: Data structures serve as the basis for abstract data types (ADT). The ADT defines the logical form of the data type. The data structure implements the physical form of the data type.
          • More precisely, a data structure is a collection of data values, the relationships among them, and the functions or operations that can be applied to the data, i.e., it is an algebraic structure about data.
Function Calls: Stack Pointer (ESP) and Frame Pointer (EBP)​

source

Der Knackpunkt ist das Register ESP (SP = Stack Pointer). Dieses enthält die Speicheradresse, die auf das letzte Element auf dem Stack zeigt.

(...), aber schöner wäre es, wenn ich mir irgendwo diejenige Speicheradresse merken könnte, die den "Anfang" des Stacks kennzeichnet.

Das ist die Idee des "Frame Pointers" oder "Base Pointers". Dafür wird normalerweise das Register EBP (BP = Base Pointer) verwendet. Ich muss mich allerdings selbst darum kümmern. Die gängige Vorgehensweise ist, beim Betritt einer "Funktion"

  • den alten Wert von EBP auf dem Stack zu speichern und dann
  • den aktuellen Wert von ESP nach EBP zu kopieren.
  • Danach kann ich ESP "beliebig" verändern und habe dennoch mit EBP immer einen festen Referenzpunkt.

Verlasse ich die Funktion wieder, muss ich natürlich aufräumen. Dies erreicht man, indem man

  • EBP zurück nach ESP kopiert und dann
  • den ehemals gespeicherten EBP-Wert vom Stack zurück nach EBP holt.

Genaueres weiter unten in source ab "Richtig interessant wird der Frame Pointer, wenn man Funktionen aufrufen möchte. (...)".

Other references:

Definitions (from cs.rutgers.edu):

  • stack frame: Each function has local memory associated with it to hold incoming parameters, local variables, and (in some cases) temporary variables. This region of memory is called a stack frame and is allocated on the process' stack.
  • frame pointer: A frame pointer (the ebp register on intel x86 architectures, rbp on 64-bit architectures) contains the base address of the function's frame. The code to access local variables within a function is generated in terms of offsets to the frame pointer.
    • Note: The stack pointer (the esp register on intel x86 architectures or rsp on 64-bit architectures) may change during the execution of a function as values are pushed or popped off the stack (such as pushing parameters in preparation to calling another function). The frame pointer doesn't change throughout the function.
Stack Overflow​

Wikipedia:

  • A stack overflow occurs if the call stack pointer (ESP, see above) exceeds the stack bound.

Causes:

Infinite recursion (most common cause of stack overflows):

int foo()
{
return foo();
}
  • space needed to store the variables and information associated with each call
  • foo() invokes itself until the stack overflows resulting in a segmentation fault (SIGSEGV in UNIX, STATUS_ACCESS_VIOLATION exception in Windows)

Very Deep Recursion:

  • a recursive function that terminates in theory but causes a call stack buffer overflow in practice
    • fix by transforming the recursion into a loop

Very large stack variables:

  • attempt to allocate more memory on the stack than will fit, e.g. by creating local array variables that are too large
int foo()
{
double x[1048576];
}

Global namespace​

  • global variables

Free store (Heap)​

Limiting the heap size​

Method 1: In the source code:

  • setrlimit(): use RLIMIT_DATA (see "2022-09-22-notes-OS.md" → "resource-limits") to limit the data segment (i.e. initialized data, uninitialized data, and heap)
    • If you try to extend the data segment past the RLIMIT_DATA limit (e.g. using brk, sbrk or some intermediary like malloc), the attempt will fail. brk or sbrk will return < 0 and malloc will return a null pointer.
  • Note on malloc:
    • usually malloc allocates memory from the heap (calling sbrk() to adjust heap size, if necessary):

      malloc doc: Normally, malloc() allocates memory from the heap, and adjusts the size of the heap as required, using sbrk(2).

    • only if memory blocks larger than MMAP_THRESHOLD are allocated, malloc will use the mmap() system call (which creates mappings in virtual memory):

      malloc doc: When allocating blocks of memory larger than MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory as a private anonymous mapping using mmap(2).

    • in order to limit the available memory for malloc use RLIMIT_DATA:

      malloc doc: Prior to Linux 4.7 allocations performed using mmap(2) were unaffected by the RLIMIT_DATA resource limit; since Linux 4.7, this limit is also enforced for allocations performed using mmap(2).

Method 2: In the bash session, where the program will be executed:

  • use ulimit -Sd kiloBytes to limit the program's data segment! (Note: kiloBytes must be sufficiently large, i.e. approx. $\gt 3000$ kiloBytes!)
  • use ulimit -Sv kiloBytes to limit the program's virtual memory (address space)! (Note: kiloBytes must be sufficiently large, i.e. approx. $\gt 3000$ kiloBytes!)
    • limiting the entire virtual memory also limits the heap!

RAM available for allocation​

When you run a C program, the program is loaded from disk into the computer's memory. The memory space the program occupies includes the program code, as well as space for all the program's static data - that is, data items that are declared in the source code. The memory left over is what's available for allocation.

Static vs. Dynamic Memory Allocation​

source

Static Memory Allocation​

  • Static memory allocation is a technique where the computer assigns memory to the program during its compilation.
  • Memory once assigned cannot vary in size, i.e. you can not increase or decrease the allocated memory size.
  • Once assigned to the program, the static memory remains throughout the program, i.e. from the moment the program is compiled upto the moment the program completes its execution. After the program executes completely, the memory is deallocated, and it can be allocated to some other entity.
  • This technique allocates the memory to the program from the stack storage.
  • In static memory allocation, the program executes faster because the memory is assigned to the program before its execution.
    • But it is quite difficult to determine how much memory would be required to run the program, and hence most of the time, memory allocation decisions are to be taken at the run time (see Dynamic Memory Allocation).

Dynamic Memory Allocation​

  • In dynamic memory allocation, the memory is allocated at the run time, i.e. when the programmer executes the program
  • this technique assigns the memory from the heap storage.
  • The memory allocated dynamically can be deallocated at any moment during the program execution.
  • Even the memory allocated can be resized, i.e. you can increase or decrease the memory size.
    • This method is more efficient as it provides the memory space according to the program's needs.
  • As the memory is allocated at the run time, this makes an execution a little slower when compared to static memory allocation.
  • The functions used to allocate and deallocate the memory dynamically are calloc(), realloc(), and free().

Allocation Functions​

  • all allocation functions return a type void pointer
    • a type void pointer must be cast to the appropriate type before being used (see section "Cast")
  • When a memory allocation function is called, you must check its return value to ensure that the memory was allocated successfully.
  • In addition, your programs must be able to gracefully handle the situation when a memory allocation request fails

malloc (stdlib.h)​

  • allocates a group of bytes
  • prototype: void *malloc(size_t num);
  • returns a pointer to the first byte
  • returns NULL
    • if the requested storage space couldn't be allocated or
    • if num == 0.

calloc (stdlib.h)​

  • allocates a group of objects
  • prototype: void *calloc(size_t num, size_t size);
  • returns a pointer to the first byte
  • clears all the allocated memory (set to 0)
  • returns NULL
    • if the requested storage space couldn't be allocated or
    • if num == 0.
    • if size == 0.

realloc (stdlib.h)​

  • changes the size of a block of memory that was previously allocated with or calloc(), malloc()
  • prototype: void *realloc(void *ptr, size_t size);
  • expand block:
    • if sufficient space exists: expand old block, return ptr
    • if sufficient space does not exist: allocate a new block, copy data to new block, free old block, return a pointer to the new block
  • if ptr == NULL: like malloc()
  • if size == 0: free(ptr)
  • if memory is insufficient: leave old block unchanged
  • returns NULL
    • if memory is insufficient (either for expanding the old block or for allocating a new one)
    • if size == 0.

Deallocation​

free (stdlib.h)​

  • releases the memory pointed to by ptr
  • prototype: void free(void *ptr);
  • must have been allocated with malloc(), calloc(), or realloc()
  • If ptr is NULL, free() does nothing
  • from "When we free a dynamically allocated memory, what happens to the pointer pointing at it?":
    • Yes, when you use a free(px); call, it frees the memory that was malloc'd earlier and pointed to by px. The pointer itself, however, will continue to exist and will still have the same address.
  • from comment under Setting variable to NULL after free by Chris Lutz (about 70,000 reputation):
    • "free(void *ptr) cannot change the value of the pointer it is passed. It can change the contents of the pointer, the data stored at that address, but not the address itself, or the value of the pointer."
Best practice​
  • After freeing a pointer ptr by free(ptr) always set ptr = NULL to avoid dangling pointer bugs!

realloc (stdlib.h)​

  • If the argument size is 0, the memory that ptr points to is freed, and the function returns NULL.

Manipulation​

  • void *memset(void *dest, int c, size_t count);
    • while c is a type int, it is treated as a type char.
    • count is the number of bytes, starting at dest, to be set
    • set all the bytes in a block of memory to a particular value
    • you can specify values of c only in the range 0 through 255
      • therefore,
        • do use memset to initialize an array of type char,
        • do not use memset to initialize arrays of type int, float or double,
          • you can initialize these arrays' elements to 0, though
  • void *memcpy(void *dest, void *src, size_t count);
    • copies bytes of data between memory blocks, sometimes called buffers
    • successive byte-for-byte copy
    • The return value is dest
    • Problem: If the two blocks of memory overlap, the function might not operate properly: some of the data in src might be overwritten before being copied. Use memmove() instead. You rarely, have a reason to use memcpy().
  • void *memmove(void *dest, void *src, size_t count);
    • like memcpy(), copying a specified number of bytes from one memory block to another, but can handle overlapping memory blocks properly

Heap Overflow​

from Wikipedia:

  • An accidental overflow may result in data corruption or unexpected behavior by any process that accesses the affected memory area.
    • On operating systems without memory protection, this could be any process on the system.

Exploitation, Vulnerability​

from Wikipedia:

  • Memory on the heap is dynamically allocated at runtime and typically contains program data.
  • Exploitation is performed by corrupting this data in specific ways to cause the application to overwrite internal structures such as linked list pointers.
  • The canonical heap overflow technique overwrites dynamic memory allocation linkage (such as malloc metadata) and uses the resulting pointer exchange to overwrite a program function pointer.
  • iOS jailbreaking often uses heap overflows to gain arbitrary code execution.
  • UAF: see Use-After-Free Vulnerability

Segmentation Fault​

  • from Wikipedia:
    • On standard x86 computers, this is a form of general protection fault.
    • The operating system kernel will, in response, usually perform some corrective action, generally passing the fault on to the offending process by sending the process a signal.
    • Processes can in some cases install a custom signal handler, allowing them to recover on their own, but otherwise the OS default signal handler is used, generally causing abnormal termination of the process (a program crash), and sometimes a core dump.
  • from stackoverflow:
    • Segmentation faults don't directly have anything to do with the heap. The term segmentation fault comes from older operating systems (Unix-style) that used "segments" of memory for different usages, and "Segmentation fault" was when the program went outside it's allocated segment. In modern systems, the memory is split into "pages" - typically 4KB each, but some processors have larger pages, and many modern processors support "large pages" of, for examble, 2MB or 1GB, which is used for large chunks of memory.
    • Now, if you use an address that points to a page that isn't there (or isn't "yours"), you get a segmentation fault. This, typically will end the application then and there. You can "trap" segmentation fault, but in all operating systems I'm aware of, it's not valid to try to continue from this "trap" - but you could for example store away some files to explain what happened and help troubleshoot the problem later, etc.

Linked List​

  • method of data storage
  • types of linked lists (each type is suited for certain types of data storage)
    • single-linked list
    • double-linked list
    • binary tree

Array vs Linked List​

from thegeekstuff:

  • a linked list is a dynamic data structure whose length can be increased or decreased at run time, whereas an array is a static data structure, i.e. the length of array cannot be altered at run time.
  • In an array, all the elements are kept at consecutive memory locations, whereas in a linked list the elements (or nodes) may be kept at any location but still connected to each other.

Single-linked list​

  • last element/node: has a pointer next being assigned the value of NULL
  • head pointer:
    • always points to the first element/node
    • if the entire list is empty (contains no links), the head pointer is set to NULL

Signal Handling​

SIGINT, ctrl - c​

Demo: See sigint_handler.c

From Why is volatile needed in C?:

Another use for volatile is signal handlers. If you have code like this:

int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}

The compiler is allowed to notice the loop body does not touch the quit variable and convert the loop to a while (true) loop. Even if the quit variable is set on the signal handler for SIGINT and SIGTERM; the compiler has no way to know that.

However, if the quit variable is declared volatile, the compiler is forced to load it every time, because it can be modified elsewhere. This is exactly what you want in this situation.

From Comments:

@AmitSinghTomar It means what it says: Every time the code checks the value, it is reloaded. Otherwise, the compiler is allowed to assume that functions that don't take a reference to the variable can't modify it, so assuming as CesarB intended that the above loop does not set quit, the compiler can optimise it into a constant loop, assuming that there's no way for quit to be changed between iterations. N.B.: This isn't necessarily a good substitute for actual threadsafe programming. - underscore_d, Jul 2, 2016 at 14:46

  • Soll heißen, volatile zu benutzen, ersetzt nicht threadsafe Programmierung! Dazu gehört noch mehr!

@PierreG. No, the compiler can always assume that the code is single-threaded, unless told otherwise. That is, in the absence of volatile or other markers, it will assume that nothing outside the loop modifies that variable once it enters the loop, even if it's a global variable. - CesarB, Sep 12, 2016 at 12:37

Other use cases for volatile in Why is volatile needed in C?.

gdb​

C Debugging​

  • when examining a function some_function by setting a breakpoint at some function b some_function, run and stopping at breakpoint some_function, you have to step into the function some_function via s. Otherwise, the code line in which the function call takes place will not be executed! This means:
    • Before running s the function arguments will not be assigned the values passed by the caller! So, e.g., if a function foo(int a) is called by caller main() via foo(4), the initialization a = 4 will happen after you run s! Before that a will have some arbitrary value.
  • ptype some_struct_instance to print out definition of struct
  • ptype /o some_struct_instance to print out definition of struct including its layout in memory (like Linux' pahole "poke-a-hole" command: Each data element of the structure has two numbers listed in C-style comments. The first number represents the offset of the data element from the start of the structure and the second number represents the size in bytes. At the end of the structure, ptype /o summarizes the total size of the structure. good pahole introduction)

Commands Doc​

Common Commands​

commanddescription
quit or ctrl + dexit gdb
help runningall the important commands you will need to know to do common things
kill or signal SIGTERMKill the child process in which your program is running under GDB.
signal SIGINT or ctrl + cinterrupt program
b 43 or b functionset a breakpoint at line 43 or at a function (wenn ein Breakpoint in einer loop ist, hält continue dort bei jeder Iteration)
d 43 or delete 43Delete the breakpoints, watchpoints, or catchpoints of the breakpoint ranges specified as arguments. If no argument is specified, delete all breakpoints (GDB asks confirmation, unless you have set confirm off).
info b or info breakpointsPrint a table of all breakpoints, watchpoints, and catchpoints set and not deleted
runrun the program in the debugger (after compiling source code with gcc -g)
record or record full or target record-full"Start process record/replay (ie. start recording the subsequent execution of the child process). You must start debugging the program (with the "run" command) before using this command to start recording. You can start recording at any point after the child process has been started (eg. at a breakpoint)." "This command starts the process record and replay target. The recording method can be specified as parameter. Without a parameter the command uses the full recording method. The following recording methods are available: full, btrace"
record stopStop process record/replay (ie. cease recording the program execution), and discard any existing execution log. The child process is not terminated, and you may continue to debug it normally.
info recordShow various statistics about the state of process record and its in-memory execution log buffer.
c or continuecontinue to next breakpoint
reverse-continue
step or sstep into function
stepi or siExecute one machine instruction (follows a call).
reverse-step
reverse-stepibackward for exactly one machine instruction
next or nstep over function
nexti or niSame as si but execute calls as one instructions.
reverse-next
reverse-nextiLike nexti in reverse -- executes one machine instruction (backward), unless that instruction is a return from a function call, in which case the entire function will be executed in reverse.
l 43 or l function or list function or l first,lastprint the source code around line 43 or the code around function definition function or the code from line first to line last (subsequently use l + or l - to read more of the code)
l +print the next 10 lines just after the lines last printed
l -print the previous 10 lines just before the lines last printed
btbacktrace command: like "Traceback (most recent call last)" in Python, but here the most recent call comes first
upGo up one stack frame.
downGo down one stack frame.
info regList contents of registers.
info f or info frameverbose description of the selected stack frame, including the address of the frame, the addresses of the next frame down (called by this frame) and the next frame up (caller of this frame), the language that the source code corresponding to this frame was written in, the address of the frame's arguments, the program counter saved in it (the address of execution in the caller frame), and which registers were saved in the frame
print exp or p exp or inspect expEvaluates and prints the value of an expression exp of the language your program is written in. If you omit exp, GDB displays the last value again (from the value history). This allows you to conveniently inspect the same value in an alternative format.
print /f expprint exp with a different format, where the letter f in \f specifies the format
x addrA more low-level way of examining data than print. Examines data in memory at a specified address addr and prints it in a specified format.
ptype typenamePrint a description of data type typename. typename may be the name of a type, or for C code it may have the form class class-name, struct struct-tag, union union-tag or enum enum-tag.

valgrind​

For detection of memory leaks.

sudo apt install valgrind

Usage:

valgrind --leak-check=yes ./a.out ARGS