Golang: How Linux distributes data

When studying the basics of the language, it’s impossible to avoid the question of how memory management works. Some specialists, while explaining, even start predicting where and what data will be located and freely use terms such as stack and heap.
But what is the situation really like? To find the answer, I turned to Linux.
1. Strings
1.1 Strings in different areas of a program
As a warm-up, let’s dig right in. Consider a program with strings located in different areas:
package main
import (
"fmt"
"time"
)
var GlobalVar string = "zzzzzzzzzzzzz"
func check() string {
funcVar := "lllllllllllll"
return funcVar
}
func main() {
var mainVar string = "xxxxxxxxxxxxxx"
mainVar = "eeeeeeeeeeeee" // rewrite
fmt.Println(check())
fmt.Println(mainVar, GlobalVar)
time.Sleep(1 * time.Hour)
}
1.1 Searching in the executable file
We disable debug info and compile:
go build -ldflags="-s -w" ./main
After building, let’s look at the executable’s sections (just in case you forgot their names):
readelf -S main
[output of sections omitted for brevity]
Searching for our values in the sections using the strings:
objdump -s main -j .rodata | grep -e eeeee -e zzzzz -e llllll -e xxxxx
Success — all our strings are in the .rodata section, close to each other.
And the string xxxxxxxxxxxxxx
was replaced and discarded by the compiler.
1.2 Analyzing the process
Now let’s try to find these same strings in a running process and at the same time get familiar with process addressing.
[process analysis with pmap
, memory dump, and strings
search]
1.3 Strings. Preliminary conclusion
The compiler placed static data in the .rodata segment, which is immutable and closely tied to .text. Interestingly, the funcVar data declared inside the function also ended up in .rodata. (But things will get even more interesting later—don’t switch channels.)
2. Variables
2.1 Pointer to a variable
Although the data itself is in .rodata, that doesn’t necessarily mean we operate on it directly. Let’s check!
[debugging with gdb
, showing Go string representation as {ptr *byte; len int}
]
Success — we confirmed how Go strings are stored.
2.2 Variable scope
Now let’s focus on variables with different scopes:
package main
import (
"fmt"
"time"
)
func privFunc() int {
funcVar := 200000
return funcVar
}
var GlobalVar int = 3000 // Global
func main() {
mainVar := 55
fmt.Println(GlobalVar, mainVar, privFunc())
time.Sleep(time.Duration(30) * time.Hour)
}
Debugging shows:
- The global variable ends up in .data.
- Local variables and function variables go into the
000000c000000000–000000c000400000
region, which Linux sees as anonymousmmap()
memory.
In Go this can be either heap or stack, and our task is to figure out which is which.
Normally local variables are stored on the stack, but the compiler may put them on the heap.
[escape analysis with go build -gcflags="-m"
]
It turns out everything escapes to the heap, including the global variable. This shows that Go’s runtime abstracts memory away from Linux. Even though data physically resides in .data, Go exposes it via the heap, managed by the garbage collector.
2.3 Comparing Go’s stack and heap
By forcing the compiler not to escape a variable (using runtime.KeepAlive
), we can keep it on the stack.
Debugging shows that stack and heap variables may live right next to each other in the same region 000000c000000000
.
3. Allocating a large memory block
We write a program that allocates 200 MB:
package main
import (
"fmt"
"runtime"
)
func main() {
data := make([]byte, 200*1024*1024) // 200 MB
for i := range data {
data[i] = byte(i % 256)
}
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
fmt.Printf("Allocated memory: %.2f MB\n", float64(memStats.Alloc)/(1024*1024))
fmt.Println("Press Enter to exit...")
fmt.Scanln()
}
The compiler reports:
make([]byte, 209715200) escapes to heap
Checking with pmap
, we see the memory in the anonymous region 000000c000000000
.
4. Memory allocation mechanism
Running under strace
shows that memory is allocated via mmap().
First, large chunks are reserved with PROT_NONE, then smaller regions are committed with PROT_READ|PROT_WRITE.
5. Conclusions
From our analysis:
- Go has its own allocation model and entities (stack, heap) independent of Linux.
- Global variables, statics, and strings are placed in .rodata and .data.
- Memory from Linux is obtained via mmap() in an anonymous region.
- Go’s heap and stack may lie side by side in the same address space, but only Go’s memory manager knows what is what.
- Memory is reserved in large regions first, then split into chunks as needed during execution.