Showing posts with label Visual Studio 2010 Express. Show all posts
Showing posts with label Visual Studio 2010 Express. Show all posts

Feb 24, 2018

Representation of data types in memory - Part 3

You have probably written statements like this thousand of times, but do you know what's going on under the hood?
float foo = 1.21;
This is the continuation of my series about representation of data types in memory. In my previous post, I've discussed the numerical integral types. Now it's time to dig into the wonderful world of floating types. There is a lot to discuss about floating types, so in this post I will focus on the Normalized form and its representation in memory.

Every C++ programmer has dealt with the data types float and double and we will soon see how they are represented in memory. But before doing that, we need to understand some basic concepts and formats, so let's discuss some theory before proceeding to the practical part.

My intention is not to give a complete description about the theory of floating points, I will give a very brief summary here (and I mean it!), the details can easily be found on the net.

Below is a very good description (from this site), which describes how a floating point is expressed when stored in memory.
A floating-point number is typically expressed in the scientific notation, with a fraction (F), and an exponent (E) of a certain radix (r), in the form of F×rE. Decimal numbers use radix of 10 (F×10E); while binary numbers use radix of 2 (F×2E).
Example:
Let's say we have 16.2510. By using the scientific notation, it can be written as 16.2510 * 100 or 1.62510 * 101 and so forth. In binary, it can be written as 10000.012 * 20, 1000.0012 * 21, 100.00012 * 2210.000012 * 23 or 1.0000012 * 24 and so forth. The representation used in a floating point is 1.0000012 * 24, which is called the Normalized form.

The IEEE standard 754 describes the single precision format and double precision format. It is important to have a brief understanding of these formats, because the floating types in C++ is based on them.

According to MSDN, following is stated (Visual Studio 2010 specific) for the data type float;
The float type is stored as a four-byte, single-precision, floating-point number. It represents a single-precision 32-bit IEEE 754 value.
According to MSDN, following is stated (Visual Studio 2010 specific) for the data type double;
The double type is stored as an eight-byte, double-precision, floating-point number. It represents a double-precision 64-bit IEEE 754 value.
Voila! There we have the basic definition of the data types float and double.

The single precision format consists of 32 bits (4 bytes), where the Most Significant Bit (MSB) represent the sign (S) bit, the following 8 bits represent the exponent (E), and the 23 Least Significant Bit(s) (LSB) represent the fraction (F). Note that a bias is applied to the exponent (E) in order to represent both positive and negative exponents. The bias is 127 in single precision format, meaning exponent (E) = 0 is represented as 127, E=1 is represented as 128 and so on.

The double precision format consists of 64 bits (8 bytes), where the MSB represent the sign (S) bit, the following 11 bits represent the exponent (E), and the 52 LSB(s) represent the fraction (F). Note that a bias is applied to the exponent (E) in order to represent both positive and negative exponents. The bias is 1023 in double precision format, meaning exponent (E) = 0 is represented as 1023, E=1 is represented as 1024 and so on.

Before proceeding to the practical part, just a few words about Normalized form.

As we have seen above, Normalized form means we have an implicit leading 1 to the left of the radix point, which is used in the fraction (F), for instance 1.0000012. This leading 1 is not represented in the 32/64 bit format, but we know the leading 1 is there, if Normalized form is used.

Let's look into a simple application. This is very similar to my previous simple application, except from the fact the data type is float.
int main()
{
   float a = 1.0;
   float b = 2.0;
   float c = 3.0;
   float d = 4.0;

   return 0;
}
When starting WinDbg and execute all the initializations statements, we see the result below.
WinDbg - Memory view after initializations
As discussed in previous posts, in the Memory view, we can see the blue rectangle which indicates the inserted block of 0xCC which is typically done in Debug mode. The blue arrow shows the offset of the Memory view, which is where the insertion of 0xCC starts. Each float is also "guarded" by four bytes 0xCC. In the Disassembly view, we can notice that special floating point instructions are used. I will not discuss them in detail here, if you want more information, they are described in Intel x86 reference manual, more specific in the section "Intel 64 and IA-32 Architectures Software Developer's Manual Volume 2A: Instruction Set Reference, A-M"

Binary representation of 1.0

To understand how a float is stored in the memory, I will work through a couple of examples. Let's start with the first statement in the simple application;
float a = 1.0;
In binary we have 1.010 = 1.02, which is equal to 1.02 * 20 in Normalized form. Now let's see how this number 1.02 * 20 is stored in memory bit by bit.
Positive number -> Sign (S) bit = 02
Exponent: 0, i.e. the exponent (E) is represented as 12710 = 011111112
Fraction (F): 02 = 000000000000000000000002
Binary representation: 00111111 10000000 00000000 000000002
Hexadecimal representation: 3F80000016

This number is stored in the memory by this instruction;
fstp    dword ptr[ebp-8h]
The fstp instruction (Store Floating Point Value) copies the value from the FPU register stack to the memory in either single- or double precision format (single in this case due to float type). Note that the fld1 (Load Floating Point Value) instruction pushed this value (1.0) onto the FPU register stack in the first place. 0x3F800000 will be stored at memory address ebp-0x8. Taking little-endian into account, each byte will be saved according to below;

ebp-0x8: 0x00
ebp-0x7: 0x00
ebp-0x6: 0x80
ebp-0x5: 0x3F

Binary representation of 2.0
float b = 2.0;
Since 1.0 already was converted above, I will give a briefer explanation below.
Binary form: 10.02
Normalized form: 1.02 * 21
Positive number -> Sign (S) bit = 02
Exponent: 1, i.e. the exponent (E) is represented as 12810 = 100000002
Fraction (F): 02 = 000000000000000000000002
Binary representation: 01000000 00000000 00000000 000000002
Hexadecimal representation: 4000000016

This number is stored in the memory by this instruction;
fstp    dword ptr[ebp-14h]
Taking little-endian into account, each byte will be saved according to below;

ebp-0x14: 0x00
ebp-0x13: 0x00
ebp-0x12: 0x00
ebp-0x11: 0x40

Binary representation of 3.0
float a = 3.0;
Binary form: 11.02
Normalized form: 1.12 * 21
Positive number -> Sign (S) bit = 02
Exponent: 1, i.e., the exponent (E) is represented as 12810 = 100000002
Fraction (F): 12 = 100000000000000000000002
Binary representation: 01000000 01000000 00000000 000000002
Hexadecimal representation: 4040000016

This number is stored in the memory by this instruction;
fstp    dword ptr [ebp-20h]
Taking little-endian into account, each byte will be saved according to below;

ebp-0x14: 0x00
ebp-0x13: 0x00
ebp-0x12: 0x40
ebp-0x11: 0x40

Binary representation of 4.0
float a = 4.0;
Binary form: 100.02
Normalized form: 1.02 * 22
Positive number -> Sign (S) bit = 02
Exponent: 2, i.e. the exponent (E) is represented as 12910 = 100000012
Fraction (F): 02 = 000000000000000000000002
Binary representation: 01000000 10000000 00000000 000000002
Hexadecimal representation: 4080000016

This number is stored in the memory by this instruction;
fstp    dword ptr[ebp-2Ch]
Taking little-endian into account, each byte will be saved according to below;

ebp-0x14: 0x00
ebp-0x13: 0x00
ebp-0x12: 0x80
ebp-0x11: 0x40

The four examples above was only dealing with numbers in Normalized form. Later, I will have a look at the Denormalized form.

You are welcome to leave comments, complaints or questions!

Jul 20, 2016

Representation of data types in memory - Part 2

This is the continuation of the series "Representation of data types in memory". In this part, I will investigate how signed numerical integers are stored in memory. I'm using Windows Vista 32 bit with Microsoft Visual C++ 2010 Express (in Debug Mode) and WinDbg. Note that I'm not considering C++11.

Well, in my previous post, we saw how the unsigned numerical integers were saved in little-endian format in a block of 0xCC. This is of course true for signed numerical integers as well, but signed integers need to take the sign into account as well.

I will start this post in a similar way like my previous one, by using the same simple program, but with signed numerical integers.
int main()
{
   int a = -1;
   int b = -2;
   int c = -3;
   int d = -4;

   return 0;
}
When starting WinDbg and execute all the initializations statements, we see the result below.

WinDbg - Memory view after initializations
As discussed before, in the Memory view, we can see the blue rectangle which indicates the inserted block of 0xCC, which typically is done in Debug mode. The blue arrow shows the offset of the Memory view, which is where the insertion of 0xCC starts. Each integer is also "guarded" by four bytes 0xCC. Now we will focus on how the signed numerical integers are saved in memory. This is done by using the two's-complement representation. What is two's-complement? You can read all about it on the net, but here is a short explanation cited from wikipedia.
"Two's complement is a mathematical operation on binary numbers, as well as a binary signed number representation based on this operation. Its wide use in computing makes it the most important example of a radix complement."
There are a lot of in-depth information on the net how two's-complement conversion is done, here is my short version;

Invert all bits of the (positive) number and add one (+1).

Let's use an actual value from our simple program as an example, for instance;
int a = -1;
Positive number: 110
It's an int, i.e. four bytes in size -> 110 = 0000000116.
Then invert all bits; 0000000116 -> FFFFFFFE16
and then add one (+1); FFFFFFFE16 -> FFFFFFFF16

In other words, the number FFFFFFFF16, represent -1 in two's complement representation.

We can see this number in the Disassembly view, more specific, we can see this instruction;
mov     dword ptr [ebp-8],0FFFFFFFFh
It means that -1 is represented as 0xFFFFFFFF and will be stored at memory address ebp-0x8. Taking little-endian into account, each byte will be saved according to below;

ebp-0x8: 0xFF
ebp-0x7: 0xFF
ebp-0x6: 0xFF
ebp-0x5: 0xFF

Now let's continue with a similar program as in the first part in this series, this time with signed numerical integers.
int main()
{
   char a = -1;
   short int b = -2;
   int c = -3;
   long int d = -4;

   return 0;
}
When starting WinDbg and execute all the initializations statements, we see the result below.
WinDbg - Memory view after initializations

Like the first simple program, the blue arrow and blue rectangle, shows the inserted 0xCC done by the rep stos instruction.
Let's investigate how each type is stored. We know they are stored in two's-complement little-endian format.

Below, I've used following shorthand to denote the two's complement conversion for each type;

Positive number (using correct size) -> Invert all bits -> Added 1

char a = -1;
0116 -> FE16 -> FF16

ebp-0x5: 0xFF
short int b = -2;
000216 -> FFFD16 -> FFFE16

ebp-0x14: 0xFE
ebp-0x13: 0xFF
int c = -3;
0000000316 -> FFFFFFFC16 -> FFFFFFFD16

ebp-0x20: 0xFD
ebp-0x1F: 0xFF
ebp-0x1E: 0xFF
ebp-0x1D: 0xFF
long int d = -4;
0000000416 -> FFFFFFFB16 -> FFFFFFFC16

ebp-0x2C: 0xFC
ebp-0x2B: 0xFF
ebp-0x2A: 0xFF
ebp-0x29: 0xFF

You are welcome to leave comments, complaints or questions!

Jul 14, 2016

Representation of data types in memory - Part 1

Normally we don't need to bother how fundamental data types are stored in memory. We just expect it to work. Recently I became curios, so I learned the details of how a fundamental data type is represented in memory, especially on the stack. As usual, I'm using Windows Vista 32 bit with Microsoft Visual C++ 2010 Express and WinDbg. Note that I'm not considering C++11.

I've divided this topic into several posts. In the first one, I will focus on one group of fundamental data types; Numerical integer types, and more specific, the unsigned ones. In following posts, I will consider the signed numerical integers, floating types and other aspects.

Well, let's look into the numerical integer types. You probably know the integer types already. Below is a recap cited from www.cplusplus.com.
"Numerical integer types:
They can store a whole number value, such as 7 or 1024. They exist in a variety of sizes, and can either be signed or unsigned, depending on whether they support negative values or not."
Let's start with a very simple program, here we just use the unsigned integer type. When compiling this program in Debug mode, and executing it, how is the unsigned integers represented in memory?
int main()
{
   unsigned int a = 1;
   unsigned int b = 2;
   unsigned int c = 3;
   unsigned int d = 4;

   return 0;
}
When starting WinDbg and execute all the initializations statements, we see the result below.
WinDbg - Memory view after initializations

The blue arrow above, indicates that I've set the Memory view to ebp-0x0F0 according to the Disassembly view. Before any initializations has been done, a block of memory is initialized to 0xCC thanks to the rep stos instruction. This is typically done in Debug mode. The rep stos instruction starts inserts 0xCC at ebp-0f0.

Further, we can also see that each initialized integer is "guarded" by four bytes of 0xCC (before and after each integer).

The x86 architecture is using the little-endian format. I will briefly explain the little-endian format here. If you want more in-depth information, just search the net.

The endianness describes how a sequence of bytes are stored in the memory. It means that the endianness only matters for data types with more than one byte in size. Little-endian means that the Least Significant Byte (LSB) is stored at the lowest address and the Most Significant Byte (MSB) is stored at the highest address.

From the example above, we are dealing with integers, so let's use integer as an example. An integer is four bytes in size, this can be seen in the Memory view. An integer can be written like "ByteA ByteB ByteC ByteD", where ByteA is the MSB and ByteD is the LSB. The memory will look like this when we are using the little-endian format.

Base Address: ByteD
Base Address + 1: ByteC
Base Address + 2: ByteB
Base Address + 3: ByteA

Let's make the example above more realistic by using an actual value from our simple program, for instance;

   unsigned int a = 1;

As we know, the unsigned int is four byte in size, so the number will be 0x00000001. According to the Disassembly view, the statement "unsigned int a = 1;", will be saved on the stack at memory address ebp-0x8. The LSB i.e. 0x01 is saved at memory address ebp-0x8, and the other bytes is saved according to below.

ebp-0x8: 0x01
ebp-0x7: 0x00
ebp-0x6: 0x00
ebp-0x5: 0x00

The example above, was only dealing with the unsigned integers. Now we move on to another simple program, which shows the data representation of each unsigned numerical integers; char, short int, int and long int.
int main()
{
   unsigned char a = 1;
   unsigned short int b = 2;
   unsigned int c = 3;
   unsigned long int d = 4;

   return 0;
}
When starting WinDbg and execute all the initializations statements, we see the result below.
WinDbg - Memory view after initializations
Like the first simple program, the blue arrow and blue rectangle, shows the inserted 0xCC done by the rep stos instruction.

Let's investigate how each type is stored. We know they are stored in little-endian format, meaning the LSB is stored in the lowest address.

   unsigned char a = 1;

ebp-0x5: 0x01

   unsigned short int b = 2;

ebp-0x14: 0x02
ebp-0x13: 0x00

   unsigned int c = 3;

ebp-0x20: 0x03
ebp-0x1F: 0x00
ebp-0x1E: 0x00
ebp-0x1D: 0x00

   unsigned long int d = 4;

ebp-0x2C: 0x04
ebp-0x2B: 0x00
ebp-0x2A: 0x00
ebp-0x29: 0x00

From the results above, we can note that the unsigned char is only one byte in size, so the endianess format does not matter. We can also see that both unsigned int and unsigned long int is four byte in size.

You are welcome to leave comments, complaints or questions!

Sep 13, 2015

Pass-by-value vs. pass-by-const-reference - Part 2

This is the continuation of the comparison of pass-by-value and pass-by-const-reference. In this post I will do comparison between user-defined classes.

As in my previous post, I'm using Visual Studio 2010 Express in Debug mode and WinDbg to view the disassembly.

First a short catch up what I've done in Part 1. We have added two integers in a function called add.

int main()
{
   int a = 3;
   int b = 7;
   int c = add(a, b);
   return 0;
}

When using the pass-by-value version of add, copies of 3 and 7 were pushed on the stack.

When using the pass-by-const-reference, the memory addresses were pushed on the stack.

This was clearly shown in the memory view of WinDbg.

I'm now going to evolve the add example above a little bit.

Let's say we have user-defined class according to below.

class MyClass
{
public:
   int a_, b_;
};

Further, let's modify the main function.

int main()
{
   MyClass myClass = {3, 7};
   int c = add(myClass);

   return 0;
}

I'm using the same idea as in Part 1. But my add function takes a MyClass type instead of two integers.

First up is the pass-by-value example. Below is the C++ code and the disassembly with the stack.

int add(MyClass myClass)
{
  return myClass.a_+myClass.b_;
}


WinDbg - disassembly of main (about to execute the call instruction) and its stack
Above, we can see that the result is pretty much as the pass-by-value example for the integers in the Part 1. We first initialize the myClass.a_ to 3, the initializing myClass.b_ to 7.
First is 7 (myClass.b_) pushed on the stack, and then is 3 (myClass.a_) pushed on the stack.

The blue and purple frames is the result of the copying, i.e. the push instructions.

Now let's continue with the disassembly of the add function.



WinDbg - disassembly of add function (about to execute the move eax instruction) and its stack
Above, follow the blue and purple lines, and you will see that the add function use the pushed values (i.e. the copies of the original values) to do the calculation.

Finally, it is time to investigate what is going on the the pass-by-const-reference case for our user-defined class.

int add(const MyClass &myClass)
{
   return myClass.a_+myClass.b_;
}


WinDbg - Disassembly of main function (about to execute the call instruction) and its stack
Above, as in the pass-by-value case, the red and green frame sets the variables myClass.a_=3 and myClass.b_=7.

The lea instruction stores the memory address of the first member in myClass, i.e 3 (myClass.a_) in eax, which is then pushed on the stack (blue frame).

Then we are ready to execute the add function, i.e. the call instruction.


WinDbg - Disassembly of add function (about to execute the move eax instruction) and its stack
The move eax, dword ptr [ebp+8] instruction, sets eax to the address of myClass.a_. Next instruction sets eax to the contents of the address which eax points to, i.e. value 3.

Same procedure is done for myClass.b_. The move ecx dword ptr [ebp+8] instruction, sets ecx to the address of myClass.a_. Next instruction add eax (3) to the contents of the address which ecx+4 points to, i.e value 7.

We have finally investigate what is happening for both integers and user-defined classes when using pass-by-value and pass-by-const-reference. When the user-defined classes grows in terms of members, the copying can be very expensive.

You are welcome to leave comments, complaints or questions!

Mar 8, 2015

Aggregate initialization

A couple of days ago, I ran into some legacy code, which basically said
Person person = {};
I hadn't really been dealing with this type of initialization before, so I had to look it up. I learned it is called aggregate initialization, and that's the topic for this post. I'm using WinDbg, Visual Studio Express 2010 and compiling in debug mode. Note also that this discussion does not take C++11 into account.

Let's say you have a struct like this.
struct Person
{
   char* name;
   int age;
   bool employed;
};
Before proceeding, we need to know what an aggregate is.
Here is the definition from C++03.
An aggregate is an array or a class (clause 9) with no user-declared constructors (12.1), no private or protected non-static data members (clause 11), no base classes (clause 10), and no virtual functions (10.3).
So the struct Person is an aggregate, which is a condition for the aggregate initialization to work.

So what does this piece of code do?
Person person = {};
I've learned that this initialization says (in this particular case) that all members in the struct will be zero-initialized.

Before proceeding with the aggregate initialization, just a quick note what may happen if you forget to initialize the struct.
The code below, will result in a struct with indeterminate values, and we get a runtime error message when executing the program.

Person person;
printf("%s, %d, %d", person.name, person.age, person.employed);



Well, back to aggregate initialization.

If we execute this piece of code, we will see that the members are zero-initialized.
#include <cstdio>

struct Person
{
   char* name;
   int age;
   bool employed;
};

int main()
{
   Person person = {};
   printf("%s, %d, %d", person.name, person.age, person.employed);

   getchar();
   return 0;
}


Execution of the program above
Above, we say that we initialize Person with an empty initialize-list.

The struct Person can be initialized using a complete initialize-list, see the example below.
 
Person person = { "Kent", 24, true };
printf("%s, %d, %d", person.name, person.age, person.employed);

Execution of the program above

It is also possible to only initialize some of the members, the remaining will be zero-initialized. In the example below, the name- and age-member will be initialized, while the employed-member will be zero-initialized.

Person person = { "Kent", 24 };
printf("%s, %d, %d", person.name, person.age, person.employed);

Execution of the program above

Before ending this post, let's check out the stack from the last example. In the screenshot below, we have just executed the initialization.


Let's clarify what's going on above. The first instruction of interest is at the virtual address 0x009213DE in the disassembly view. As you can see, the struct's Person's name-member is initialized with a 32 bit pointer. This pointer (within the red box) points to the string literal "Kent" (which probably is within a read-only section in the memory).

Further, we see that the struct's Person's age-member is initialized with 18h, i.e. 24.

Finally, the last member in the struct Person, the employed-member, is zero-initialized, using the xor eax, eax instruction.

The stack is shown to the left, as you can see within the blue box, the current stack frame (EBP) start at virtual address 0x0025FA14. The boxes in the stack view shows where the Person's member are located and its values.

You are welcome to leave comments, complaints or questions!

Jan 19, 2015

Pass-by-value vs. pass-by-const-reference - Part 1

You probably already know the difference between pass-by-value and pass-by-const-reference. However, before proceeding, below follows a brief explanation.

Pass-by-value will result in a copy operation, since the function use a copy of the original input parameter. This can be a very expensive operation, especially for user-defined classes with a lot of members (and base classes).

In a function, which use pass-by-const-reference, no copies are used, the function use the original input parameter (but can not modify it, since it declared const).

This post is actually the first post of two, where I'm comparing the disassembly of pass-by-value code vs. pass-by-const-reference code. In this post, the comparison is done with built-in types.

In the next post, I'm doing the same comparison, but for user-defined classes.

I'm using Visual Studio 2010 Express and building in Debug mode. Why? Because I'm demonstrating very simple examples, which means the compiler will do a lot of optimizations in Release mode. Further, I'm using WinDbg to view the disassembly.

Are you familiar with the calling convention cdecl? This calling convention is used by default for each function you are writing in Visual Studio. What you need to know in this context, is that cdecl will put the arguments on the stack in reversed order. It will be clearer when we are stepping through the examples below.

Okay, let's checkout the example. Below is a very simple program which adds two integers. The addition is done in the function add.

int main()
{
   int a = 3;
   int b = 7;
   int c = add(a, b);

   return 0;
}
 
As mentioned above, I'm going to compare the pass-by-value case with the pass-by-const-reference.

First up is the pass-by-value case. Below is the C++ code and the disassembly of the main function with its stack.

int add(int a, int b)
{
   return a+b;
}

WinDbg - disassembly of main (about to execute the call instruction) and its stack
Okay, let's see what's going on above. The first instruction of interest is the instruction in the red frame. Here we set variable a=3. Next instruction (within the green frame) set the variable b=7.

Then we are ready to execute the add function, i.e. the call instruction. But before making the call, we must put the arguments on stack. Remember that the add function use the cdecl calling convention, which means we pass the arguments to the stack in reversed order, i.e. first push 7 (b), and then push 3 (a).

Also note what's occurs on the stack. The red and green frames is the original values of a and b. The blue and purple frames is the result of the copying, i.e. the push instructions.

Now let's continue with the disassembly of the add function.


WinDbg - disassembly of add function (about to execute the move eax instruction) and its stack
Above, follow the blue and purple lines, and you will see that the add function use the pushed values (i.e. the copies of the original values) to do the calculation.

Next up is the pass-by-const-reference case. Below is the C++ code and the disassembly of the main function with its stack.

int add(int &a, int &b)
{
   return a+b;
}


WinDbg - Disassembly of main function (about to execute the call instruction) and its stack

Above, as in the pass-by-value case, the red and green frame sets the variables a=3 and b=7. But after the initiate of the variables, there is a difference. We push two values to the stack, but not copies of the values, but addresses to the values instead.

The first lea instruction stores the address of 7 (b) in eax, which is then pushed on the stack. The second lea instruction stores the address of 3 (a) in ecx, which is then pushed on the stack.

Then we are ready to execute the add function, i.e. the call instruction.

Also check out the stack. The red and green frames is the original values of a and b (exactly as in the pass-by-value case). The blue and purple frames contains the address of b and a (instead of copies as in the pass-by-value vase).

Now let's continue with the disassembly of the add function.


WinDbg - Disassembly of add function (about to execute the move eax instruction) and its stack
Above, follow the blue and purple lines, and you will see that the add function use the addresses of b and a to do the calculation.

The move eax, dword ptr [ebp+8] instruction, sets eax to the address of a. Next instruction sets eax to the contents of the address which eax points to, i.e. value 3.

Same procedure is done for b, which is then added to 3 (a) in the instruction add eax, dword ptr [ecx].

To summarize, in the pass-by-value case, copies of the values are pushed on the stack. In the pass-by-const-reference case, the addresses of the values are pushed on the stack.

In the next part of comparing pass-by-value and pass-by-const-reference, I will have a look at user-defined classes.

You are welcome to leave comments, complaints or questions!

Jan 5, 2015

PE file with empty main()

If you build an empty console program, how many bytes is needed for the Portable Executable (PE)? And what's inside the PE file?

In this post I'm experimenting with an empty console program. I'm reducing the PE file, so it just contains the headers and the binary machine code. I'm using Visual Studio 2010 Express and building the PE files in Release mode. Further, I'm using PE Insider from Cerbero and PEBrowserPro from Smidgeonsoft.

Let's compile and link the following empty program below.
int main()
{
   return 0;
}

A program doing nothing. So simple as it can be. Now, let's check out the size of this program, using file properties.

File properties - size of file


Alright, 6144 bytes of binary data is needed for this empty program. Why is this size needed, and what kind of binary data is in there? Let's fire up PE Insider, and first check out the header size, and then the section table.

PE Insider - size of headers

PE Insider - section table

Above, the SizeOfHeaders present the (HEX)size of the headers, and the SizeOfRawData, present the (HEX)size for each section needed on disk.

Let's try to understand the number 6144. This is actually the sum of the size of the headers and the size of the sections. So let's sum it to verify this:

0x400+0x800+0x600+0x200+0x200+0x200 = 0x1800 (DEC: 6144)

Okay, now we understand why the size is 6144. But what's inside all this binary data?

First let's check out the .text section, i.e. the code. This section needs 0x800 bytes. However, my empty program is doing nothing!

PE Insider - .text section

Above, the .text section starts at offset 0x400 (file on disk), and there is a lot of things going on.

Again, the main function is doing nothing. But there is a lot of other code around in the .text section. It's code from the C Runtime library. For instance, the main function is not the first function called when executing the program, the first function called is the mainCRTStartup function. This is a function in the C Runtime library (and part of the PE file). How do I know this? Well, each program has an entry point, which is specified in one of the headers in the PE file. Let's check it out.

PE Insider - Entry point
 
PEBrowserPro - disassemble view of entry point

Okay, the entry point is at Relative Virtual Address (RVA) 0x12A0 (of course within the .text section). Thanks to the disassemble view from PEBrowserPro, we can see what's going on there. At this RVA, the mainCRTStartup is located.

Next test to do; let's tell Visual Studio to use another entry point (i.e. not the mainCRTStartup). We can do this in the Property Pages dialog.


Visual Studio - specifying my own entry point

After compiling and linking, let's check the file properties again.

File properties - size of file

Wow! The file size is reduced! From 6144 bytes to 3072 bytes. Let's check out the section table again.

PE Insider - section table

Comparing to the screenshots above, the .text section is reduced from 0x800 to 0x200, the .rdata section from 0x600 to 0x200, the .data section 0x200 to 0x000. The .rsrc and the .reloc section remain the same size.

So how do the .text section look like now when we specified our own entry point?

PE Insider - .text section (main entry point)


PEBrowserPro - disassemble view - .text section (main entry point)

The only thing going on in the .text section is the return 0 statement. We have managed to get rid of the C Runtime Library code.

Note that there is a lot of 0's in the .text section. This is just zeropadding, so each section can start at a multiple of 0x200.

Now let's continue with the other sections in the PE file. The purpose for the .reloc section, is to help the loader to do some relocation if the executable not is loaded at its preferred load address. If we remove the DYNAMICBASE switch, the .reloc section will be removed. This means that the executable always will be loaded at its preferred load address, and does not need any relocation information. Let's remove the DYNAMICBASE switch.


Visual Studio - Remove dynamicbase

 Next, compile and link again and check out the file properties, and the section table again.

File properties - size of file
PE Insider - section table

Voila! The file size is decreased from 3072 bytes to 3036 bytes. The .reloc section is gone.

Let's continue with the other sections. What's going on in the .rdata section and the .rsrc section?

PE Insider - .rdata section
 
PE Insider - .rsrc section

Thanks to the ASCII view, we can figure out that Visual Studio embed a manifest in .rsrc section. We can also see that there is a PDB path in the .rdata section.

If we want to get rid of this, we just go to the Property Pages and remove the manifest as well as the debug information.


Visual Studio - Removing manifest



Visual Studio - Removing debugging information

Again, compile and link and check out the file size, the section table, and the .rdata section.

File properties - size of file
PE Insider - section table
PE Insider - .rdata section, remaining data

Not much left. File size is reduced from 3036 bytes to 2048 bytes. The .rsrc section is gone. The .text section contains a couple of bytes of binary machine code and the .data section is empty. There remain some data in the .rdata section. I'm not completely sure what this data is. If you know, please tell me. However, it can be removed by saying no to Whole Program Optimization.


Visual Studio - Whole Program Optimization

Compile and link the PE file and let's have a final look at the file properties and the section table.

File properties - size of file


PE Insider - section table

Well, that's about it! The file size is reduced from 2048 bytes to 1024 bytes. The PE file now just contains the headers and the .text section with a couple of binary machine codes.

You are welcome to leave comments, complaints or questions!