Showing posts with label stack. Show all posts
Showing posts with label stack. 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!

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!

Dec 21, 2014

Do you know where the stack begin?

There is a lot of information on the net, showing the concepts of a stack data structure. I bet you know how a stack works. I also bet you are familiar with the registers esb and esp. You have probably seen them got some funny values in your favorite debugger. But do you know where the stack begin?

In this post, I will point out the user-mode stack's start address, using WinDbg, for a single threaded Win32 console program "Hello world!", written in Visual Studio 2010 Express, using Windows Vista 32 bit.

So, let's check out my "Hello world" program.
#include <cstdio>

int main()
{
   std::printf("Hello world!");

   return 0;
}
No surprise. It is a classical "Hello world!" program. It is also a single threaded Win32 console program.

If you didn't know it, there are user-mode threads and kernel-mode threads. Each thread has its own stack. The user-mode thread has its own stack and kernel-mode thread has its own stack. In this post, we only focus on the user-mode stack. In the "Hello world!" program, there is only one thread, i.e. one user-mode stack. There is something called Thread Environment Block (TEB, also called TIB), which contains information about the thread. Each thread has its own TEB, so for a multithreaded program, there are a lot of TEB's around. You can find the TEB (TEB's) in the process virtual memory space, in the user-mode part of the memory. The information within the TEB, tells us where the stack begins for this particular thread.

Well, le'ts not go into detail of the TEB. We want to check out the information in the TEB, and that's very easy in WinDbg.

Let's fire up WinDbg and open the "Hello world!" program (note that it is built in Release mode). In the prompt, I've used the !teb command, which displays the contents of the current thread. As mentioned before, there is only one thread running in this program.

WinDbg - debug break after loading modules


Above, the StackBase tells us that the stack begins at the virtual address 0x00230000. You can also note that the esp value, during this first debug break, is 0x0022f694. Let's take the difference between these two values (remember that the stack grows to lower addresses). The difference is 0x96C (DEC: 2412), which means that there are already 2412 bytes on the stack when the first debug break was executed.

As you can see in the screenshot above, I've also added a breakpoint in the main function. I've done that so we can see the stack status before executing the printf.

Let's continue the execution of the program.

By writing g in the WinDbg prompt, we continue the execution. Below follows a set of screenshots, showing what's going on when we break in the main() function.

WinDbg - diassembly view - before executing push instruction


WinDbg - Register view - before executing push instruction


Above, in the Disassembly view, we are about to execute the push instruction. This means that we are about to push the offset (virtual address 0x00e520f4 in this case) to the string literal "Hello world!", i.e. push a 4 byte value. The string literal itself is within the .rdata section of the PE file (helloworld.exe).

As you can see in the Register view, the esp value is 0x22f3e4. Taking the difference to the StackBase, we get 0xC1C (DEC: 3100), i.e. when entering main(), there are already 3100 bytes on the stack.

Before ending this post, let's check out another execution of the "Hello world!" program via WinDbg.

WinDbg - debug break after loading modules (another execution)
Apparently, the StackBase got another value, 0x00160000. It can also be mentioned that the "Hello world!" program is ASLR compatible. I guess that's the reason why we get another StackBase. Maybe it will be a discussion for another post.

It is probably not very helpful to know the start address of the stack in your daily programming activities, but it always fun learn something new.