Learn to Code via Tutorials on Repl.it!

← Back to all posts
Memory packing in C
h
MocaCDeveloper (598)

Hi!

In this tutorial you will learn the basics over how memory is aligned in C(using structs), and how it changes via packing it.
You will be learning:
1. How to use __attribute__((packed))
2. #pragma pack(push, n) where n will be 1, 2, 4 or 8
3. #pragma pack(pop)
4. #pragma pack(n) where n will be 1, 2, 4 or 8.

So. Lets get started.

How is memory aligned?

So. Lets say we have a file, rf.h. And, in this file, we have a struct.

Here is rf.h:

#ifndef RF
#define RF

    typedef struct RS
    {
        int a;
        int b;
    } RS_;

#endif

The struct RS_ has 2 variables. int a and int b.
Now. Lets see how the memory would be aligned:

[a(1)] -> 0x000
[a(2)] -> 0x001
[a(3)] -> 0x002
[a(4)] -> 0x003
[b(1)] -> 0x004
[b(2)] -> 0x005
[b(3)] -> 0x006
[b(4)] -> 0x007

Integers have 4 bytes. So, as you can see, there is no padding. Now, lets add a char to the struct!

Here is the code now:

#ifndef RF
#define RF

    typedef struct RS
    {
        int a;
        int b;
        char c;
    } RS_;

#endif

Now, lets see how the memory alignment would look like now:

[a(1)] -> 0x000
[a(2)] -> 0x001
[a(3)] -> 0x002
[a(4)] -> 0x003
[b(1)] -> 0x004
[b(2)] -> 0x005
[b(3)] -> 0x006
[b(4)] -> 0x007
[c(1)] -> 0x008
[pad ]
[pad ]
[pad ]

As you can see, there are 3 bytes of padding.
Why?

Well. When aligning memory, it reads 4 bytes at a time(this depends on what system you're running, of course).
So, since a char is just 1 byte, we have 3 leftover bytes. For the processor to be more efficient, the rest of the bytes are "padded". Otherwise, we'd have a abstract outline of the memory alignment.

Lets take a look at what the memory alignment would look like if we didn't pad the rest of the memory.

Lets add another integer after the char variable:

#ifndef RF
#define RF

    typedef struct RS
    {
        int a;
        int b;
        char c;
        int ab;
    } RS_;

#endif

Lets see what this would look like if the rest of the 3 bytes after the char c is aligned was not padded:

[a(1)]  -> 0x000
[a(2)]  -> 0x001
[a(3)]  -> 0x002
[a(4)]  -> 0x003
[b(1)]  -> 0x004
[b(2)]  -> 0x005
[b(3)]  -> 0x006
[b(4)]  -> 0x007
[c(1)]  -> 0x008
[ab(1)] -> 0x009
[ab(2)] -> 0x010
[ab(3)] -> 0x011
[ab(4)] -> 0x012

Now, lets take a look at the alignment with the padding:

[a(1)]  -> 0x000
[a(2)]  -> 0x001
[a(3)]  -> 0x002
[a(4)]  -> 0x003
[b(1)]  -> 0x004
[b(2)]  -> 0x005
[b(3)]  -> 0x006
[b(4)]  -> 0x007
[c(1)]  -> 0x008
[pad ]
[pad ]
[pad ]
[ab(1)] -> 0x012
[ab(2)] -> 0x013
[ab(3)] -> 0x14
[ab(4)] -> 0x15

Notice how the first byte of the integer variable ab is at a "easy" address(0x012) instead of 0x09?
This makes it easier for the processor to access the first byte of memory, otherwise it would have to have some type of special way to be able to locate where the first byte of the next variable starts.

This is called processor-efficiency. If there is any leftover bytes, they will be padded.
So, lets change the way the struct looks to put this into perspective:

#ifndef RF
#define RF

    typedef struct RS
    {
        char c;
        int a;
        int b;
        int ab;
    } RS_;

#endif

The memory alignment(with padding) would be:

[c(1)]  -> 0x000
[pad ]  -> 0x001
[pad ]  -> 0x002
[pad ]  -> 0x003
[a(1)]  -> 0x004
[a(2)]  -> 0x005
[a(3)]  -> 0x006
[a(4)]  -> 0x007
[b(1)]  -> 0x008
[b(2)]  -> 0x009
[b(3)]  -> 0x010
[b(4)]  -> 0x011
[ab(1)]  -> 0x012
[ab(2)]  -> 0x013
[ab(3)]  -> 0x014
[ab(4)]  -> 0x015

Notice how the first byte of each variable starts at a easily distributed address?(Ex: 0x008, 0x012)
This makes it efficient for the processor to read.

Now, without that padding of the char, the first byte of a would be at 0x001 and the processor would have to do more work to find and access that first byte.

Now, what is "packing" the memory?

As simple as it sounds. You can probably guess what it means.
When you "pack" the memory, you basically tell the compiler to change how exactly it pads the rest of the bytes.

Lets take a look at the first possible way to do this:

#pragma pack(push, n)

#pragma pack(push, n)
This pushes the alignment on to an eternal stack and then sets the new alignment.

n can be -> 1, 2, 4 or 8.

Lets take a look!

#ifndef RF
#define RF

#pragma pack(push, 2)
typedef struct RS
{
    int a;
    char b;
    int c;
} RS_;

#endif

Here, we set the new alignment to 2 bits.
Lets take a look at what the memory alignment would thus look like:

|  1   |  2   |
| a(1) | a(2) |
| a(3) | a(4) |
| b(1) | pad. |
| c(1) | c(2) |
| c(3) | c(4) |

The size of the struct would then be 2 x 5 = 10.

Simple enough, all #pragma pack(push, n) does is set the alignment on an eternal stack and sets the new alignment.

What is #pragma pack(n) do?

Primarily the same as pack(push, n) only it does not set the alignment on an eternal stack. It just sets the new alignment. So, take the example from above, and just remove the push.

What is #pragma pack(pop)?

#pragma pack(pop) restores the alignment to the one saved at the top of the eternal stack.

Simple enough. Just add #pragma pack(pop) after defining your structs.

Quick Note: Memory packing is usually used in memory dense projects in C. Memory packing is usually performed on structs. Rarely will you see memory packing using on variables themselves.

What is __attribute__((packed))?

__attribute__((packed)) basically just tells the compiler to do no padding, whatsoever.

So, lets take a look at an example of using this:

#ifndef RF
#define RF

typedef __attribute__((packed)) struct RS
{
    char a;
    int b;
} RS_;

#endif

The memory alignment would then be:

[a(1)] -> 0x000
[b(1)] -> 0x001
[b(2)] -> 0x002
[b(3)] -> 0x003
[b(4)] -> 0x004

Notice anything?
If you look closely, you will notice that there is no padding for the leftover 3 bytes after the first byte of the variable a was written.

All __attribute__((packed)) does is take away any padding assigned during memory alignment.

This is good for memory efficient/processor efficient projects, such as OSDev, or any other project working with the OS or memory-dense concepts.

This is not as useful as it may seem, but to have the standard knowledge on packing memory in C could be very helpful someday, and just knowing how memory alignment is performed(and how it's aligned) is a useful skill to have.

Practice packing memory, and play around with it a little. It's not all that hard to pack memory. The harder part is knowing how the memory is aligned so you understand your program.

If you're a C developer, and you're looking to get into some low low-level projects, memory packing is something you might run across, and knowing how your memory is aligned will be helpful so you don't end up with memory leaks!

C GOD logging out!

Comments
hotnewtop
DynamicSquid (4782)

This is an awesome tutorial! Never knew you could memory pack in C before!

MocaCDeveloper (598)

@DynamicSquid

Sorry for the late reply!

I didn't know you could pack memory in C either. I was just at school looking up something I had seen in a github repo that was written in C and it had all these cool ideals I had never seen before.

And so I started going through each one and I got into memory packing and I was astounded by it. It's quite interesting to say the least that you are capable of changing how much static memory the program takes.

I explained both concepts of memory packing and how the memory was aligned in this tutorial otherwise it would be quite useless to know how to pack the memory but not know how the memory alignment works. I feel like knowing how the memory is aligned is a big part of writing C code, and being able to follow the code and know just where your memory is going(or how much memory you're going to be stealing).