How to diagnose and locate segmentation faults in x86 assembly
Segmentation faults commonly occur when programs attempt to access memory regions that they are not allowed to access. This article provides an overview of segmentation faults with practical examples. We will discuss how segmentation faults can occur in x86 assembly as well as C along with some debugging techniques.
What are segmentation faults in x86 assembly?
A segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (for example, attempting to write to a read-only location).
Let us consider the following x86 assembly example.
message db “Welcome to Segmentation Faults! ”
mov ecx, message
As we can notice, the preceding program calls the subroutine _printMessage when it is executed. When we read this program without executing, it looks innocent without any evident problems. Let us assemble and link it using the following commands.
Now, let us run the program and observe the output.
As we can notice in the preceding excerpt, there is a segmentation fault when the program is executed.
How to detect segmentation faults in x86 assembly
Segmentation faults always occur during runtime but they can be detected at the code level. The previous sample program that was causing the segmentation faults, is due to lack of an exit routine within the program. So when the program completes executing the code responsible for printing the string, it doesn’t know how to exit and thus lands on some invalid memory address.
Another way to detect segmentation faults is to look for core dumps. Core dumps are usually generated when there is a segmentation fault. Core dumps provide the situation of the program at the time of the crash and thus we will be able to analyze the crash. Core dumps must be enabled on most systems as shown below.
| ulimit -c unlimited |
When a segmentation fault occurs, a new core file will be generated as shown below.
Welcome to Segmentation Faults! Segmentation fault (core dumped)
core seg seg.nasm seg.o
As shown in the proceeding excerpt, there is a new file named core in the current directory.
How to fix segmentation faults x86 assembly
Segmentation faults can occur due to a variety of problems. Fixing a segmentation fault always depends on the root cause of the segmentation fault. Let us go through the same example we used earlier and attempt to fix the segmentation fault. Following is the original x86 assembly program causing a segmentation fault.
message db “Welcome to Segmentation Faults! ”
mov ecx, message
As mentioned earlier, there isn’t an exit routine to gracefully exit this program. So, let us add a call to the exit routine immediately after the control is returned from _printMessage. This looks as follows
message db “Welcome to Segmentation Faults! ”
mov ecx, message
_exit:
mov eax, 1
mov ebx, 0
int 0x80
Notice the additional piece of code added in the preceding excerpt. When _printMessage completes execution, the control will be transferred to the caller and call _exit instruction will be executed, which is responsible for gracefully exiting the program without any segmentation faults. To verify, let us assemble and link the program using the following commands.
Run the binary and we should see the following message without any segmentation fault.
Welcome to Segmentation Faults!
As mentioned earlier, the solution to fix a segmentation fault always depends on the root cause.
How to fix segmentation fault in c
Segmentation faults in C programs are often seen due to the fact that C programming offers access to low-level memory. Let us consider the following example written in C language.
The preceding program causes a segmentation fault when it is run. The string variable str in this example stores in read-only part of the data segment and we are attempting to modify read-only memory using the line *(str+1) = ‘x’;
Similarly, segmentation faults can occur when an array out of bound is accessed as shown in the following example.
This example also leads to a segmentation fault. In addition to it, if the data being passed to the test variable is user-controlled, it can lead to stack-based buffer overflow attacks. Running this program shows the following error due to a security feature called stack cookies.
*** stack smashing detected ***: terminated
Aborted (core dumped)
The preceding excerpt shows that the out-of-bound access on an array can also lead to segfaults. Fixing these issues in C programs again falls back to the reason for the Segfault. We should avoid accessing protected memory regions to minimize segfaults.
How to debug segmentation fault
Let us go through our first x86 example that was causing a segfault to get an overview of debugging segmentation faults using gdb. Let us begin by running the program, so we can get the core dump when the segmentation fault occurs.
Welcome to Segmentation Faults! Segmentation fault (core dumped)
Now, a core dump should have been generated. Let us load the core dump along with the target executable as shown in the following command. Loading the executable along with the core dump makes the debugging process much easier.
GEF for linux ready, type `gef’ to start, `gef config’ to configure
78 commands loaded for GDB 9.1 using Python engine 3.8
[*] 2 commands could not be loaded, run `gef missing` to know why.
Core was generated by `./print’.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0804901c in ?? ()
As we can notice in the preceding output, the core dump is loaded using GDB and the segmentation fault occurred at the address 0x0804901c. To confirm this, we can check the output of info registers.
ecx 0x804a000 0x804a000
esp 0xffbe8aa0 0xffbe8aa0
eip 0x804901c 0x804901c
eflags 0x10202 [ IF RF ]
As highlighted, the eip register contains the same address. This means, the program attempted to execute the instruction at this address and it has resulted in a segmentation fault. Let us go through the disassembly and understand where this instruction is.
First, let us get the list of functions available and identify which function possibly caused the segfault.
All defined functions:
0x08049000 _printMessage
0x08049017 _start
As highlighted in the preceding excerpt, the _printMessage and _start functions’ address ranges are close to the address that caused the segmentation fault. So, let us begin with the disassembly of the function _printMessage.
Dump of assembler code for function _printMessage:
0x08049000 <+0>: mov eax,0x4
0x08049005 <+5>: mov ebx,0x1
0x0804900a <+10>: mov ecx,0x804a000
0x0804900f <+15>: mov edx,0x20
0x08049014 <+20>: int 0x80
0x08049016 <+22>: ret
End of assembler dump.
Let us set a breakpoint at ret instruction and run the program. The following command shows how to setup the breakpoint.
Breakpoint 1 at 0x8049016
Type run to start the program execution.
0x804900f <_printMessage+15> mov edx, 0x20
0x8049014 <_printMessage+20> int 0x80
→ 0x8049016 <_printMessage+22> ret
↳ 0x804901c add BYTE PTR [eax], al
0x804901e add BYTE PTR [eax], al
0x8049020 add BYTE PTR [eax], al
0x8049022 add BYTE PTR [eax], al
0x8049024 add BYTE PTR [eax], al
As we can notice in the preceding excerpt, when the ret instruction gets executed, the control gets passed to the region not controlled by the program code leading to unauthorized memory access and thus a segmentation fault.
Conclusion
This article has outlined some basic concepts around segmentation faults in x86 assembly and how one can use them for debugging programs. We have seen various simple examples to better understand the concepts. We briefly discussed core dumps, which can help us to detect and analyze program crashes.
Debugging Segmentation Faults and Pointer Problems
By Alex Allain
For new programmers, debugging errors associated with pointers can be a nightmare. «Segmentation Fault (core dumped)» is a pretty vague error message, and it’s even worse when strange bugs start appearing that don’t cause segmentation faults — but that result in things like memory getting overwritten in unexpected ways. Fortunately, tools like Cee Studio, from our sponsor, provide new, easier ways to debug memory errors and buffer overflows. Cee Studio is a web-based compiler designed to make finding segfaults easy by providing instant and informative feedback on misuses of memory. But even without specialized tools, finding problems with pointers is easier than you think.
This tutorial assumes that you have a basic knowledge of pointers such as can be acquired by reading a pointer tutorial. It would help to be running a system that has a debugger such as GDB, or to at least have sufficient familiarity with GDB-like debuggers to understand the examples presented.
What is a segmentation fault?
When your program runs, it has access to certain portions of memory. First, you have local variables in each of your functions; these are stored in the stack. Second, you may have some memory, allocated during runtime (using either malloc, in C, or new, in C++), stored on the heap (you may also hear it called the «free store»). Your program is only allowed to touch memory that belongs to it — the memory previously mentioned. Any access outside that area will cause a segmentation fault. Segmentation faults are commonly referred to as segfaults.
There are four common mistakes that lead to segmentation faults: dereferencing NULL, dereferencing an uninitialized pointer, dereferencing a pointer that has been freed (or deleted, in C++) or that has gone out of scope (in the case of arrays declared in functions), and writing off the end of an array.
A fifth way of causing a segfault is a recursive function that uses all of the stack space. On some systems, this will cause a «stack overflow» report, and on others, it will merely appear as another type of segmentation fault.
The strategy for debugging all of these problems is the same: load the core file into GDB, do a backtrace, move into the scope of your code, and list the lines of code that caused the segmentation fault.
For instance, running on a Linux system, here’s an example session: This just loads the program called example using the core file called «core». The core file contains all the information needed by GDB to reconstruct the state of execution when the invalid operation caused a segmentation fault.
Once we’ve loaded up gdb, we get the following: So, execution stopped inside the function called foo() on line 4, which happened to be the assignment of the number 3 to the location pointed to by x. This is a goldmine of information: we already know exactly where the problem happened and which pointer was involved.
Since this is a somewhat contrived example, we can immediately find the error. The pointer x is initialized to 0, equivalent to NULL (in fact, NULL is a stand-in for 0), and we know that it’s a no-no to then try to access that pointer.
But what if it weren’t so obvious? Simply printing the value of the pointer can often lead to the solution. In this case: Printing out x reveals that it points to memory address 0x0 (the 0x indicates that the value following it is in hexadecimal, traditional for printing memory addresses). The address 0x0 is invalid — in fact, it’s NULL. If you dereference a pointer that stores the location 0x0 then you’ll definitely get a segmentation fault, just as we did.
If we’d gotten something more complicated, such as execution crashing inside a system call or library function (perhaps because we passed an uninitialized pointer to fgets), we’d need to figure out where we called the library function and what might have happened to cause a segfault within it. Here’s an example from another debugging session: This time, the segfault occurred because of something inside strcat. Does this mean the library function did something wrong? Nope! It means that we probably passed a bad value to the function. To debug this, we need to see what we passed into strcat.
So let’s see what function call we made that led to the segfault. Backtrace lists the function calls that had been made at the time the program crashed. Each function is directly above the function that called it. So foo was called by main in this case. The numbers on the side (#0, #1, #2) also indicate the order of calls, from most recent to longest ago.
To move from viewing the state within each function (encapsulated in the idea of a stack frame), we can use the up and down commands. Right now, we know we’re in the strcat stack frame, which contains all of the local variables of strcat, because it’s the top function on the stack. We want to move «up» (toward the higher numbers); this is the opposite of how the stack is printed. This helps a little — we know that we have a variable called x and a constant string. We should probably lookup the strcat function at this point to make sure that we got the order of arguments correct. Since we did, the problem must be with x. There it is again: a NULL pointer. The strcat function must be derefencing a NULL pointer that we gave it, and even though it’s a library function, it doesn’t do anything magical.
NULL pointers are generally pretty easy to work with — once we’ve found one, we know that somewhere along the line, we didn’t allocate some memory that we should have. It’s just a question of where. A common mistake is to not check the return from malloc to make sure that the system isn’t out of memory. Another common mistake is to assume that a function that calls malloc doesn’t return NULL even though it returns the result of malloc. Note that in C++, when you call new, it will throw an exception, bad_alloc, if sufficient memory cannot be allocated. Your code should be prepared to handle this situation cleanly, and if you choose to catch the exception and return NULL inside a function that ordinarily returns a new’ed pointer, this advice still holds. We did a good thing by checking to make sure that malloc succeeds before using the memory in create_memory, but we don’t check to make sure that create_memory returns a valid pointer! Shame on us. This is a bug that won’t catch you until you’re running your code on a real system unless you explicitly test your code in low memory situations.
Dereferencing an Uninitialized Pointer
Figuring out whether or not a pointer has been initialized is a bit harder than figuring out whether a pointer is NULL. The best way to avoid using an uninitialized pointer is to set your pointers to NULL when you declare them (or immediately initialize them). That way, if you do use a pointer that hasn’t had memory allocated for it, you will immediately be able to tell.
If you don’t set your pointers to NULL when you declare them, then you’ll have a much harder time of it (remember that non-static variables aren’t automatically initialized to anything in C or C++). You might need to figure out if 0x4025e800 is valid memory. One way you can get a sense of this in GDB is by printing out the addresses stored in other pointers you’ve allocated. If they’re fairly close together, you’ve probably correctly allocated memory. Of course, there’s no guarantee that this rule of thumb will hold on all systems.
In some cases, your debugger can tell you that an address is invalid based on the value stored in the pointer. For instance, in the following example, GDB indicates that the char* x, which I set to point to the memory address «30», is not accessible. Generally, though, the best way to handle such a situation is just to avoid having to rely on memory’s being close together or obviously invalid. Set your variables to NULL from the beginning.
Dereferencing Freed Memory
This is another tricky bug to find because you’re working with memory addresses that look valid. The best way to handle such a situation is again preventative: set your pointer to point to NULL as soon as you’ve freed it. That way, if you do try to use it later, then you’ll have another «dereferencing NULL» bug, which should be much easier to track.
Another form of this bug is the problem of dealing with memory that has gone out of scope. If you declare a local array such as then the array, x, will no longer be valid once the function returns. This is a really tricky bug to find because once again the memory address will look valid when you print it out in GDB. In fact, your code might even work sometimes (or just display weird behavior by printing whatever happens to be on the stack in the location that used to be the memory of the array x). Generally, the way you’ll know if you have this kind of bug is that you’ll get garbage when you print out the variable even though you know that it’s initialized. Watch out for the pointers returned from functions. If that pointer is causing you trouble, check the function and look for whether the pointer is pointing to a local variable in the function. Note that it is perfectly fine to return a pointer to memory allocated in the function using new or malloc, but not to return a pointer to a statically declared array (e.g., char x[10]).
Tools such as Valgrind can be immensely helpful in tracking down these bugs because they watch memory to ensure that it’s valid. If it isn’t, Valgrind will alert you. Our Valgrind tutorial goes into more detail about finding this sort of bug.
Of course, the best solution is simply to avoid ever doing anything like this. Technically, you could use a static buffer, which would allow you to have a permanent buffer you could pass around. But this is only asking for trouble if you later decide, for whatever reason, that you don’t need it to be static (if you forget why you made it static in the first place, for instance).
Writing off the end of the array
Generally, if you’re writing off the bounds of an array, then the line that caused the segfault in the first place should be an array access. (There are a few times when this won’t actually be the case — notably, if the fact that you wrote off an array causes the stack to be smashed — basically, overwriting the pointer that stores where to return after the function completes.)
Of course, sometimes, you won’t actually cause a segfault writing off the end of the array. Instead, you might just notice that some of your variable values are changing periodically and unexpectedly. This is a tough bug to crack; one option is to set up your debugger to watch a variable for changes and run your program until the variable’s value changes. Your debugger will break on that instruction, and you can poke around to figure out if that behavior is unexpected. This approach can get tricky when you’re dealing with a lot of dynamically allocated memory and it’s not entirely clear what you should watch. To simplify things, use simple test cases, keep working with the same inputs, and turn off randomized seeds if you’re using random numbers!
Stack Overflows
A stack overflow isn’t the same type of pointer-related problem as the others. In this case, you don’t need to have a single explicit pointer in your program; you just need a recursive function without a base case. Nevertheless, this is a tutorial about segmentation faults, and on some systems, a stack overflow will be reported as a segmentation fault. (This makes sense because running out of memory on the stack will violate memory segmentation.)
To diagnose a stack overflow in GDB, typically you just need to do a backtrace: If you find a single function call piling up an awfully large number of times, this is a good indication of a stack overflow.
Typically, you need to analyze your recursive function to make sure that all the base cases (the cases in which the function should not call itself) are covered correctly. For instance, in computing the factorial function In this case, the base case of n being zero is covered, but what about n < 0? On «valid» inputs, the function will work fine, but not on «invalid» inputs like -1.
You also have to make sure that your base case is reachable. Even if you have the correct base case, if you don’t correctly progress toward the base case, your function will never terminate.
Summary
While segmentation faults can be nasty and difficult to track down when you are first learning to program, over time you will start to see them as falling into a small number of patterns that are relatively easy to track down. This tutorial hasn’t covered every possible scenario for causing segmentation faults, but it touches on many of the basic problems you may encounter.
Segmentation Fault (распределение памяти компьютера)

Когда я делаю ошибку в коде, то обычно это приводит к появлению сообщения “segmentation fault”, зачастую сокращённого до “segfault”. И тут же мои коллеги и руководство приходят ко мне: «Ха! У нас тут для тебя есть segfault для исправления!» — «Ну да, виноват», — обычно отвечаю я. Но многие ли из вас знают, что на самом деле означает ошибка “segmentation fault”?
Чтобы ответить на этот вопрос, нам нужно вернуться в далёкие 1960-е. Я хочу объяснить, как работает компьютер, а точнее — как в современных компьютерах осуществляется доступ к памяти. Это поможет понять, откуда же берётся это странное сообщение об ошибке.
Вся представленная ниже информация — основы компьютерной архитектуры. И без нужды я не буду сильно углубляться в эту область. Также я буду применять всем известную терминологию, так что мой пост будет понятен всем, кто не совсем на «вы» с вычислительной техникой. Если же вы захотите изучить вопрос работы с памятью подробнее, то можете обратиться к многочисленной доступной литературе. А заодно не забудьте покопаться в исходном коде ядра какой-нибудь ОС, например, Linux. Я не буду излагать здесь историю вычислительной техники, некоторые вещи не будут освещаться, а некоторые сильно упрощены.
Немного истории
Когда-то компьютеры были очень большими, весили тонны, при этом обладали одним процессором и памятью примерно на 16 Кб. Стоил такой монстр порядка $150 000 и мог выполнять лишь одну задачу за раз: в каждый момент времени выполнялся только один какой-то процесс. Архитектуру памяти в те времена можно схематически представить так:

То есть на ОС приходилась, скажем, четверть всей доступной памяти, а остальной объём отдавался под пользовательские задачи. В то время роль ОС заключалась в простом управлении оборудованием с помощью прерываний ЦПУ. Так что операционке нужна была память для себя, для копирования данных с устройств и для работы с ними (режим PIO). Для вывода данных на экран нужно было использовать часть основной памяти, ведь видеоподсистема либо не имела своей оперативки, либо обладала считанными килобайтами. А уже сама программа выполнялась в области памяти, идущей сразу после ОС, и решала свои задачи.
Совместный доступ к ресурсам
Главная проблема заключалась в том, что устройство, стоящее $150 000, было однозадачным и тратило целые дни на обработку нескольких килобайт данных.
Из-за непомерной стоимости мало кто мог позволить себе приобрести сразу несколько компьютеров, чтобы обрабатывать одновременно несколько задач. Поэтому люди начали искать способы совместного доступа к вычислительным ресурсам одного компьютера. Так наступила эра многозадачности. Обратите внимание, что в те времена ещё никто не помышлял о многопроцессорных компьютерах. Так как же можно заставить компьютер с одним ЦПУ выполнять несколько разных задач?
Решением стало использование планировщика задач (scheduling): пока один процесс прерывался, ожидая завершения операций ввода/вывода, ЦПУ мог выполнять другой процесс. Я не буду здесь больше касаться планировщика задач, это слишком обширная тема, не имеющая отношения к памяти.
Если компьютер способен поочерёдно выполнять несколько задач, то распределение памяти будет выглядеть примерно так:

Задачи А и В хранятся в памяти, поскольку копировать их на диск и обратно слишком затратно. И по мере того, как процессор выполняет ту или иную задачу, он обращается к памяти за соответствующими данными. Но тут возникает проблема.
Когда один программист будет писать код для выполнения задачи В, он должен знать границы выделяемых сегментов памяти. Допустим, задача В занимает в памяти отрезок от 10 до 12 Кб, тогда каждый адрес памяти должен быть жёстко закодирован в пределах этих границ. Но если компьютер будет выполнять сразу три задачи, то память будет поделена на большее количество сегментов, и значит сегмент для задачи В может оказаться сдвинут. Тогда код программы придётся переписывать, чтобы она могла оперировать меньшим объёмом памяти, а также изменить все указатели.
Здесь всплывает и иная проблема: что если задача В обратится к сегменту памяти, выделенному для задачи А? Такое легко может произойти, ведь при работе с указателями памяти достаточно сделать маленькую ошибку, и программа будет обращаться к совершенно другому адресу, нарушив целостность данных другого процесса. При этом задача А может работать с очень важными с точки зрения безопасности данными. Нет никакого способа помешать В вторгнуться в область памяти А. Наконец, вследствие ошибки программиста задача В может перезаписать область памяти ОС (в данном случае от 0 до 4 Кб).
Адресное пространство
Чтобы можно было спокойно выполнять несколько задач, хранящихся в памяти, нам нужна помощь от ОС и оборудования. В частности, адресное пространство. Это некая абстракция памяти, выделяемая ОС для какого-то процесса. На сегодняшний день это фундаментальная концепция, которая используется везде. По крайней мере, во ВСЕХ компьютерах гражданского назначения принят именно этот подход, а у военных могут быть свои секреты. Персоналки, смартфоны, телевизоры, игровые приставки, умные часы, банкоматы — ткните в любой аппарат, и окажется, что распределение памяти в нём осуществляется по принципу «код-стек-куча» (code-stack-heap).
Адресное пространство содержит всё, что нужно для выполнения процесса:
- Машинные инструкции, которые должен выполнить ЦПУ.
- Данные, с которыми будут работать эти машинные инструкции.

- Стек (stack) — это область памяти, в которой программа хранит информацию о вызываемых функциях, их аргументах и каждой локальной переменной в функциях. Размер области может меняться по мере работы программы. При вызове функций стек увеличивается, а при завершении — уменьшается.
- Куча (heap) — это область памяти, в которой программа может делать всё, что заблагорассудится. Размер области может меняться. Программист имеет возможность воспользоваться частью памяти кучи с помощью функции malloc() , и тогда эта область памяти увеличивается. Возврат ресурсов осуществляется с помощью free() , после чего куча уменьшается.
- Кодовый сегмент (code) — это область памяти, в которой хранятся машинные инструкции скомпилированной программы. Они генерируются компилятором, но могут быть написаны и вручную. Обратите внимание, что эта область памяти также может быть разделена на три части (текст, данные и BSS). Эта область памяти имеет фиксированный размер, определяемый компилятором. В нашем примере пусть это будет 1 Кб.
Виртуализация памяти
Допустим, задача А получила в своё распоряжение всю доступную пользовательскую память. И тут возникает задача В. Как быть? Решение было найдено в виртуализации.
Напомню одну из предыдущих иллюстраций, когда в памяти одновременно находятся А и В:

Допустим, А пытается получить доступ к памяти в собственном адресном пространстве, например по индексу 11 Кб. Возможно даже, что это будет её собственный стек. В этом случае ОС нужно придумать, как не подгружать индекс 1500, поскольку по факту он может указывать на область задачи В.
На самом деле, адресное пространство, которое каждая программа считает своей памятью, является памятью виртуальной. Фальшивкой. И в области памяти задачи А индекс 11 Кб будет фальшивым адресом. То есть — адресом виртуальной памяти.
Каждая программа, выполняющаяся на компьютере, работает с фальшивой (виртуальной) памятью. С помощью некоторых чипов ОС обманывает процесс, когда он обращается к какой-либо области памяти. Благодаря виртуализации ни один процесс не может получить доступ к памяти, которая ему не принадлежит: задача А не влезет в память задачи В или самой ОС. При этом на пользовательском уровне всё абсолютно прозрачно, благодаря обширному и сложному коду ядра ОС.
Таким образом, каждое обращение к памяти регулируется операционной системой. И это должно осуществляться очень эффективно, чтобы не слишком замедлять работу различных выполняющихся программ. Эффективность обеспечивается с помощью аппаратных средств, преимущественно — ЦПУ и некоторых компонентов вроде MMU. Последний появился в виде отдельного чипа в начале 1970-х, а сегодня MMU встраиваются непосредственно в процессор и в обязательном порядке используются операционными системами.
Вот небольшая программка на С, демонстрирующая работу с адресами памяти:
На моей машине LP64 X86_64 она показывает такой результат:
Code is at 0x40054c
Stack is at 0x7ffe60a1465c
Heap is at 0x1ecf010
Как я и описывал, сначала идёт кодовый сегмент, затем куча, а затем стек. Но все эти три адреса фальшивые. В физической памяти по адресу 0x7ffe60a1465c вовсе не хранится целочисленная переменная со значением 3. Никогда не забывайте, что все пользовательские программы манипулируют виртуальными адресами, и только на уровне ядра или аппаратных драйверов допускается использование адресов физической памяти.
Переадресация
Переадресация (транслирование, перевод, преобразование адресов) — это термин, обозначающий процесс сопоставления виртуального адреса физическому. Занимается этим модуль MMU. Для каждого выполняющегося процесса операционка должна помнить соответствия всех виртуальных адресов физическим. И это довольно непростая задача. По сути, ОС приходится управлять памятью каждого пользовательского процесса при каждом обращении. Тем самым она превращает кошмарную реальность физической памяти в полезную, мощную и лёгкую в использовании абстракцию.
Давайте рассмотрим подробнее.
Когда запускается процесс, ОС бронирует для него фиксированный объём физической памяти, пусть это будет 16 Кб. Начальный адрес этого адресного пространства сохраняется в специальной переменной base . А в переменной bounds записывается размер выделенной области памяти, в нашем примере — 16 Кб. Эти два значения записываются в каждую таблицу процессов — PCB (Process Control Block).
Итак, это виртуальное адресное пространство:

А это его физический образ:

ОС решает выделить диапазон физических адресов от 4 до 20 Кб, то есть значение base равно 4 Кб, а значение bounds равно 4 + 16 = 20 Кб. Когда процесс ставится в очередь на выполнение (ему выделяется процессорное время), ОС считывает из PCB значения обеих переменных и копирует их в специальные регистры ЦПУ. Далее процесс запускается и пытается обратиться, допустим, к виртуальному адресу 2 Кб (в своей куче). К этому адресу ЦПУ добавляет значение base , полученное от ОС. Следовательно, физический адрес будет 2+ 4 = 6 Кб.
Физический адрес = виртуальный адрес + base
Если получившийся физический адрес (6 Кб) выбивается из границ выделенной области (4—20 Кб), это означает, что процесс пытается обратиться к памяти, которая ему не принадлежит. Тогда ЦПУ генерирует исключение и сообщает об этом ОС, которая обрабатывает данное исключение. В этом случае система обычно сигнализирует процессу о нарушении: SIGSEGV, Segmentation Fault. Этот сигнал по умолчанию прерывает выполнение процесса (это можно настраивать).
Перераспределение памяти
Если задача А исключена из очереди на выполнение, то это даже лучше. Это означает, что планировщик попросили выполнить другую задачу (допустим, В). Пока выполняется В, операционка может перераспределить всё физическое пространство задачи А. Во время выполнения пользовательского процесса ОС зачастую теряет управление процессором. Но когда процесс делает системный вызов, процессор снова возвращается под контроль ОС. До этого системного вызова операционка может что угодно делать с памятью, в том числе и целиком перераспределять адресное пространство процесса в другой физический раздел.
В нашем примере это осуществляется достаточно просто: ОС перемещает 16-килобайтную область в другое свободное место подходящего размера и просто обновляет значения переменных base и bounds для задачи А. Когда процессор возвращается к её выполнению, процесс переадресации всё ещё работает, но физическое адресное пространство уже изменилось.
С точки зрения задачи А ничего не меняется, её собственное адресное пространство по-прежнему расположено в диапазоне 0-16 Кб. При этом ОС и MMU полностью контролируют каждое обращение задачи к памяти. То есть программист манипулирует виртуальной областью 0-16 Кб, а MMU берёт на себя сопоставление с физическими адресами.
После перераспределения образ памяти будет выглядеть так:

Программисту теперь не нужно заботиться о том, с какими адресами памяти будет работать его программа, не нужно переживать о конфликтах. ОС в связке с MMU снимают с него все эти заботы.
Сегментация памяти
В предыдущих главах мы рассмотрели вопросы переадресации и перераспределения памяти. Однако у нашей модели работы с памятью есть ряд недостатков:
- Мы предполагаем, что каждое виртуальное адресное пространство имеет размер в 16 Кб. Это не имеет никакого отношения к действительности.
- ОС приходится поддерживать список свободных диапазонов физической памяти размером по 16 Кб, чтобы выделять их для новых запускаемых процессов или перераспределения текущих выделенных областей. Как можно эффективно осуществлять всё это, не ухудшив производительность всей системы?
- Мы выделяем по 16 Кб каждому процессу, но ведь не факт, что каждый из них будет использовать всю выделенную область. Так что мы просто теряем кучу памяти на пустом месте. Это называется внутренней фрагментацией (internal fragmentation) — память резервируется, но не используется.
Для решения некоторых из этих проблем давайте рассмотрим более сложную систему организации памяти — сегментацию. Смысл её прост: принцип “base and bounds” распространяется на все три сегмента памяти — кучу, кодовый сегмент и стек, причём для каждого процесса, вместо того чтобы рассматривать образ памяти как единую уникальную сущность.
В результате мы больше не теряем память между стеком и кучей:

Как вы могли заметить, свободное пространство в виртуальной памяти задачи А больше не размещено в памяти физической. И память теперь используется гораздо эффективнее. ОС теперь должна запоминать для каждой задачи три пары base и bounds , по одной для каждого сегмента. MMU, как и раньше, занимается переадресацией, но оперирует уже тремя base
и тремя bounds .
Допустим, у кучи задачи А параметр base равен 126 Кб, а bounds — 2 Кб. Пусть задача А обращается к виртуальному адресу 3 Кб (в куче). Тогда физический адрес определяется как 3 – 2 Кб (начало кучи) = 1 Кб + 126 Кб (сдвиг) = 127 Кб. Это меньше 128, а значит ошибки обращения не будет.
Совместное использование сегментов
Сегментирование физической памяти не только не позволяет виртуальной памяти отъедать физическую, но также даёт возможность совместного использования физических сегментов с помощью виртуальных адресных пространств разных процессов.
Если дважды запустить задачу А, то кодовый сегмент у них будет один и тот же: в обеих задачах выполняются одинаковые машинные инструкции. В то же время у каждой задачи будут свои стек и куча, поскольку они оперируют разными наборами данных.

При этом оба процесса не подозревают, что делят с кем-то свою память. Такой подход стал возможен благодаря внедрению битов защиты сегмента (segment protection bits).
Для каждого создаваемого физического сегмента ОС регистрирует значение bounds , которое используется MMU для последующей переадресации. Но в то же время регистрируется и так называемый флаг разрешения (permission flag).
Поскольку сам код нельзя модифицировать, то все кодовые сегменты создаются с флагами RX. Это значит, что процесс может загружать эту область памяти для последующего выполнения, но в неё никто не может записывать. Другие два сегмента — куча и стек — имеют флаги RW, то есть процесс может считывать и записывать в эти свои два сегмента, однако код из них выполнять нельзя. Это сделано для обеспечения безопасности, чтобы злоумышленник не мог повредить кучу или стек, внедрив в них свой код для получения root-прав. Так было не всегда, и для высокой эффективности этого решения требуется аппаратная поддержка. В процессорах Intel это называется “NX bit”.
Флаги могут быть изменены в процессе выполнения программы, для этого используется mprotect().
Под Linux все эти сегменты памяти можно посмотреть с помощью утилит /proc/
Вот пример на PHP:
Здесь есть все необходимые подробности относительно распределения памяти. Адреса виртуальные, отображаются разрешения для каждой области памяти. Каждый совместно используемый объект (.so) размещён в адресном пространстве в виде нескольких частей (обычно код и данные). Кодовые сегменты являются исполняемыми и совместно используются в физической памяти всеми процессами, которые разместили подобный совместно используемый объект в своём адресном пространстве.
Shared Objects — это одно из крупнейших преимуществ Unix- и Linux-систем, обеспечивающее экономию памяти.
Также с помощью системного вызова mmap() можно создавать совместно используемую область, которая преобразуется в совместно используемый физический сегмент. Тогда у каждой области появится индекс s, означающий shared.
Ограничения сегментации
Итак, сегментация позволила решить проблему неиспользуемой виртуальной памяти. Если она не используется, то и не размещается в физической памяти благодаря использованию сегментов, соответствующих именно объёму используемой памяти.
Но это не совсем верно.
Допустим, процесс запросил у кучи 16 Кб. Скорее всего, ОС создаст в физической памяти сегмент соответствующего размера. Если пользователь потом освободит из них 2 Кб, тогда ОС придётся уменьшить размер сегмента до 14 Кб. Но вдруг потом программист запросит у кучи ещё 30 Кб? Тогда предыдущий сегмент нужно увеличить более чем в два раза, а возможно ли это будет сделать? Может быть, его уже окружают другие сегменты, не позволяющие ему увеличиться. Тогда ОС придётся искать свободное место на 30 Кб и перераспределять сегмент.

Главный недостаток сегментов заключается в том, что из-за них физическая память сильно фрагментируется, поскольку сегменты увеличиваются и уменьшаются по мере того, как пользовательские процессы запрашивают и освобождают память. А ОС приходится поддерживать список свободных участков и управлять ими.
Фрагментация может привести к тому, что какой-нибудь процесс запросит такой объём памяти, который будет больше любого из свободных участков. И в этом случае ОС придётся отказать процессу в выделении памяти, даже если суммарный объём свободных областей будет существенно больше.
ОС может попытаться разместить данные компактнее, объединяя все свободные области в один большой чанк, который в дальнейшем можно использовать для нужд новых процессов и перераспределения.

Но подобные алгоритмы оптимизации сильно нагружают процессор, а ведь его мощности нужны для выполнения пользовательских процессов. Если ОС начинает реорганизовывать физическую память, то система становится недоступной.
Так что сегментация памяти влечёт за собой немало проблем, связанных с управлением памятью и многозадачностью. Нужно как-то улучшить возможности сегментации и исправить недостатки. Это достигается с помощью ещё одного подхода — страниц виртуальной памяти.
Разбиение памяти на страницы
Как было сказано выше, главный недостаток сегментации заключается в том, что сегменты очень часто меняют свой размер, и это приводит к фрагментации памяти, из-за чего может возникнуть ситуация, когда ОС не выделит для процессов нужные области памяти. Эта проблема решается с помощью страниц: каждое размещение, которое ядро делает в физической памяти, имеет фиксированный размер. То есть страницы — это области физической памяти фиксированного размера, ничего более. Это сильно облегчает задачу управления свободным объёмом и избавляет от фрагментации.
Давайте рассмотрим пример: виртуальное адресное пространство объёмом 16 Кб разбито на страницы.

Мы не говорим здесь о куче, стеке или кодовом сегменте. Просто делим память на куски по 4 Кб. Затем то же самое делаем с физической памятью:

ОС хранит таблицу страниц процесса (process page table), в которой представлены взаимосвязи между страницей виртуальной памяти процесса и страницей физической памяти (страничный кадр, page frame).

Теперь мы избавились от проблемы поиска свободного места: страничный кадр либо используется, либо нет (unused). И ядру не в пример легче найти достаточное количество страниц, чтобы выполнить запрос процесса на выделение памяти.
Страница — это мельчайшая и неделимая единица памяти, которой может оперировать ОС.
У каждого процесса есть своя таблица страниц, в которой представлена переадресация. Здесь уже используются не значения границ области, а номер виртуальной страницы (VPN, virtual page number) и сдвиг (offset).
Пример: размер виртуального пространства 16 Кб, следовательно, нам нужно 14 бит для описания адресов (2 14 = 16 Кб). Размер страницы 4 Кб, значит нам нужно 4 Кб (16/4), чтобы выбрать нужную страницу:

Когда процесс хочет использовать, например, адрес 9438 (вне границ 16 384), то он запрашивает в двоичном коде 10.0100.1101.1110:

Это 1246-й байт в виртуальной странице номер 2 («0100.1101.1110»-й байт в «10»-й странице). Теперь ОС достаточно просто обратиться к таблице страниц процесса, чтобы найти эту страницу номер 2. В нашем примере она соответствует восьмитысячному байту физической памяти. Следовательно, виртуальный адрес 9438 соответствует физическому адресу 9442 (8000 + сдвиг 1246).
Как уже было сказано, каждый процесс обладает лишь одной таблицей страниц, поскольку у каждого процесса собственная переадресация, как и у сегментов. Но где же именно хранятся все эти таблицы? Наверное, в физической памяти, где же ещё им быть?
Если сами таблицы страниц хранятся в памяти, то для получения VPN надо обращаться к памяти. Тогда количество обращений к ней удваивается: сначала мы извлекаем из памяти номер нужной страницы, а затем обращаемся к самим данным, хранящимся в этой странице. И если скорость доступа к памяти невелика, то ситуация выглядит довольно грустно.
Буфер быстрой переадресации (TLB, Translation-lookaside Buffer)
Использование страниц в качестве основного инструмента поддержки виртуальной памяти может привести к сильному снижению производительности. Разбиение адресного пространства на небольшие куски (страницы) требует хранения большого количества данных о размещении страниц. А раз эти данные хранятся в памяти, то при каждом обращении процесса к памяти осуществляется ещё одно, дополнительное обращение.
Для поддержания производительности снова используется помощь оборудования. Как и при сегментации, мы аппаратными методами помогаем ядру эффективно осуществлять переадресацию. Для этого используется TLB, входящий в состав MMU, и представляющий собой простой кэш для некоторых VPN-переадресаций. TLB позволяет ОС не обращаться к памяти лишний раз, чтобы получить физический адрес из виртуального.
Аппаратный MMU инициируется при каждом обращении к памяти, извлекает из виртуального адреса VPN и запрашивает у TLB, хранится ли в нём переадресация с этого VPN. Если да, то его роль выполнена. Если нет, то MMU находит нужную таблицу страниц процесса, и если она ссылается на валидный адрес, то обновляет данные в TLB, чтобы тот предоставлял их при следующем обращении.
Как вы понимаете, если в кэше отсутствует нужная переадресация, то это замедляет обращение к памяти. Можно предположить, что чем больше размер страниц, тем больше вероятность, что в TLB окажутся нужные данные. Но тогда мы будем тратить больше памяти на каждую страницу. Так что здесь нужен какой-то компромисс. Современные ядра умеют использовать страницы разных размеров. Например, Linux способен оперировать «огромными» страницами по 2 Мб вместо традиционных 4 Кб.
Также рекомендуется хранить данные компактно, в смежных адресах памяти. Если вы раскидаете их по всей памяти, то куда чаще в TLB не будет обнаруживаться нужной переадресации, либо он будет постоянно переполняться. Это называется эффективностью пространственной локальности (spacial locality efficiency): данные, которые расположены в памяти сразу за вашими, могут размещаться в той же физической странице, и тогда благодаря TLB вы получите выигрыш в производительности.
Кроме того, TLB в каждой записи хранит так называемые ASID (Address Space Identifier, идентификатор адресного пространства). Это нечто вроде PID, идентификатора процесса. Каждый процесс, поставленный в очередь на выполнение, имеет собственный ASID, и TLB может управлять обращением любого процесса к памяти, без риска ошибочных обращений со стороны других процессов.
Повторимся снова: если пользовательский процесс пытается обратиться к неправильному адресу, тот наверняка будет отсутствовать в TLB. Следовательно, будет запущена процедура поиска в таблице страниц процесса. В ней хранится переадресация, но с неправильным набором битов. В х86-системах переадресации имеют размер 4 Кб, то есть битов в них немало. А значит есть вероятность найти правильный бит, равно как и другие вещи, наподобие бита изменения («грязного бита», dirty bit), битов защиты (protection bit), бита обращения (reference bit) и т.д. И если запись помечена как неправильная, то ОС по умолчанию выдаст SIGSEGV, что приведёт к ошибке “segmentation fault”, даже если о сегментах уже и речи не идёт.
На самом деле разбиение памяти на страницы в современных ОС устроено куда сложнее, чем я расписал. В частности, используются многоуровневые записи в таблицах страниц, многостраничные размеры, вытеснение страниц (page eviction), также известное как «обмен» (ядро скидывает страницы из памяти на диск и обратно, что повышает эффективность использования основной памяти и создаёт у процессов иллюзию её неограниченности).
Segmentation fault (core dumped) — to where? what is it? and why?
When a segmentation fault occurs in Linux, the error message Segmentation fault (core dumped) will be printed to the terminal (if any), and the program will be terminated. As a C/C++ dev, this happens to me quite often, and I usually ignore it and move onto gdb , recreating my previous action in order to trigger the invalid memory reference again. Instead, I thought I might be able to perhaps use this «core» instead, as running gdb all the time is rather tedious, and I cannot always recreate the segmentation fault.
My questions are three:
- Where is this elusive «core» dumped?
- What does it contain?
- What can I do with it?
![]()
4 Answers 4
If other people clean up .
. you usually don’t find anything. But luckily Linux has a handler for this which you can specify at runtime. In /usr/src/linux/Documentation/sysctl/kernel.txt you will find:
core_pattern is used to specify a core dumpfile pattern name.
- If the first character of the pattern is a ‘|’, the kernel will treat the rest of the pattern as a command to run. The core dump will be written to the standard input of that program instead of to a file.
According to the source this is handled by the abrt program (that’s Automatic Bug Reporting Tool, not abort), but on my Arch Linux it is handled by systemd. You may want to write your own handler or use the current directory.
But what’s in there?
Now what it contains is system specific, but according to the all knowing encyclopedia:
[A core dump] consists of the recorded state of the working memory of a computer program at a specific time[. ]. In practice, other key pieces of program state are usually dumped at the same time, including the processor registers, which may include the program counter and stack pointer, memory management information, and other processor and operating system flags and information.
. so it basically contains everything that gdb needs (in addition to the executable that caused the fault) to analyze the fault.
Yeah, but I’d like me to be happy instead of gdb
You can both be happy since gdb will load any core dump as long as you have a exact copy of your executable: gdb path/to/binary my/core.dump . You should then be able to analyze the specific failure instead of trying and failing to reproduce bugs.
Also, if ulimit -c returns 0 , then no core dump file will be written.
You can also trigger a core dump manually with CTRL — \ which quits the process and causes a core dump.
![]()
The core file is normally called core and is located in the current working directory of the process. However, there is a long list of reasons why a core file would not be generated, and it may be located somewhere else entirely, under a different name. See the core.5 man page for details: