이번 포스팅에서는 The Art of Debugging with GDB, DDD and EclipseChatper 4 WHEN A PROGRAM CRASHES 내용을 번역해보았습니다. 해당 챕터의 앞부분에서는 가상 주소 공간에서의 페이지(page)에 대한 개념을 소개하고 seg fault가 발생하는 원인을 페이지(page)와 연관지어 잘 설명해주고 있습니다.


목차

  1. 1. Background Material: Memory Management
    1. 1.1. Why does a program crash?
    2. 1.2. Program layout in memory
    3. 1.3. The notion of pages
    4. 1.4. Details on the role of the page table
    5. 1.5. A slight memory-access bug might not cause a seg fault

Background Material: Memory Management

Why does a program crash?

단연코 충돌이 발생하는 가장 일반적인 원인은 프로그램이 접근 권한이 없는 메모리 공간에 접근하려고 시도하기 때문입니다.

By far the most common cause of a crash is for a program to attempt to access a memory location without having the permission to do so.


Unix 계열의 플랫폼에서는 일반적으로 프로그램이 segmentation fault(seg fault로 알려져있는)를 발생시켰다는 것을 알려주고 해당 프로그램의 실행을 중단할 것입니다.

On Unix-family platforms, the OS will normally announce that the program has caused a segmentation fault, commonly referred to as a seg fault, and discontinue execution of the program.


seg fault를 처리하기 위한 용도로 GDB를 효율적으로 사용하기 위해서는, 메모리 접근 에러가 어떻게 발생하는지에 대해 정확히 이해하는 것이 중요합니다.

In order to effectively use GDB to deal with seg faults, it is important to understand exactly how memory access errors occur.


Program layout in memory

Unix 플랫폼에서 프로그램에 할당된 가상 주소들의 집합은 Figure 4-1 그림과 같은 형태로 배치되어있습니다.

On Unix platforms, a program’s set of allocated virtual addresses typically is laid out something like the diagram in Figure 4-1.


text section은 컴파일러가 당신의 프로그램 소스 코드를 통해 생성해낸 기계 지시어들로 구성되어있습니다.

The text section consists of the machine instructions produced by the compiler from your program’s source code.


data section은 컴파일 타임에 할당되는 모든 프로그램 변수들을 포함합니다. (초기화된 변수는 data영역에 초기화되지않은 변수는 bss영역에 저장됨)

The data section contains all the program variables that are allocated at compile time.


당신의 프로그램이 런타임에 OS로부터 추가적인 메모리를 요청하는 경우에 해당 메모리는 heap이라는 공간에 할당됩니다.

When your program requests additional memory from the operating system at run time the requested memory is allocated in an area called the heap.


stack section은 동적으로 할당된 데이터를 위한 공간입니다. 함수 호출 시 사용되는 인자들이나 지역 변수들 그리고 반환 주소값들이 stack 공간에 저장됩니다. stack의 크기는 함수 호출이 발생할 때 증가하고 호출자에게 함수가 다시 반환될때 감소합니다.

The stack section is space for dynamically allocated data. The data for function calls — including arguments, local variables, and return addresses — are stored on the stack. The stack grows each time a function call is made and shrinks each time a function returns to its caller.


The notion of pages

가상 주소 공간은 페이지라고 불리는 덩어리(chunk)들의 구성으로 표현됩니다. RAM이나 ROM과 같은 물리적 메모리 또한 페이지들로 나누어진 무언가로 표현됩니다.

A virtual address space is viewed as organized into chunks called pages. Physical memory (both RAM and ROM) is also viewed as divided into pages.


실행에 의해 프로그램이 메모리에 로드될 때, OS는 프로그램의 몇몇 페이지들을 물리적 메모리의 페이지에 저장되도록 배열시킵니다. 이러한 페이지들을 resident라고 부르며 resident를 제외한 나머지 페이지들은 디스크에 저장됩니다.

When a program is loaded into memory for execution, the OS arranges for some of the pages of the program to be stored in pages of physical memory. These pages are said to be resident, and the rest are stored on disk.


지금 현재는 resident(물리적 메모리의 페이지에 저장된 프로그램의 페이지)가 아닌 몇몇 프로그램 페이지들이 프로그램이 실행 중인 동안의 다양한 시점에 필요할 수 있습니다. 이러한 시점에 그것은 하드웨어에 의해 감지되고 이를 통해 당신의 프로그램은 제어권을 OS로 넘겨주게 됩니다. OS는 필요한 프로그램의 페이지를 메모리로 로딩하고나서 제어권 다시 프로그램에게 넘겨줍니다. 이 과정에서 더이상 resident가 아닌 프로그램 페이지(= nonresident)는 디스크에 저장될 것입니다.

At various times during execution, some program page that is not currently resident will be needed. When this occurs, it will be sensed by the hardware, which transfers control to the OS. The latter brings the required page into memory and then returns control to our program. The evicted program page, if any, becomes nonresident and will be stored on disk.


위의 모든 과정을 관리하기 위해서 OS는 각각의 프로세스마다 페이지 테이블이라는 것을 유지하게 됩니다. 프로세스의 가상 페이지들 각각은 테이블 내에 entry를 가지고 있으며 이 entry에는 아래 정보들을 포함하고 있습니다.

To manage all of this, the OS maintains a page table for each process. Each of the process’s virtual pages has an entry in the table, which includes the following information:


  • 페이지의 메모리 혹은 디스크 상의 현재 물리적 위치 정보
  • 페이지의 read, write, execute 권한 정보
  • The current physical location of this page in memory or on disk.
  • Permissions — read, write, execute — for this page.

OS가 부분적인 페이지들을 프로그램에 할당하는 것이 아니라는 것에 주목합시다. 예를 들어, 실행 중인 프로그램이 약 10,000 bytes의 사이즈를 갖는다고 한다면, 이 프로그램이 온전히 로드됐을 경우 메모리의 3개 페이지를 차지(페이지 기본 사이즈가 4,096 bytes인 경우)할 것입니다. 페이지는 VM 시스템에 의해 조작되는 메모리의 가장 작은 단위이기 때문에 약 2.5 페이지를 차지한다거나 하지는 않을것입니다. 바로 이 지점이 디버깅 시에 매우 중요한 포인트입니다. 왜냐하면 이것은 프로그램에 의해 발생하는 몇몇 잘못된 메모리 접근들이 seg fault를 발생시키지 않을 수 있음을 의미하기 때문입니다.

Note that the OS will not allocate partial pages to a program. For example, if the program to be run has a total size of about 10,000 bytes, it would occupy three pages of memory if fully loaded. It would not merely occupy about 2.5 pages, as pages are the smallest unit of memory manipulated by the VM system. This is an important point to understand when debugging, because it implies that some erroneous memory accesses by the program will not trigger seg faults, as you will see below.


다시 말해, 디버깅 세션에 있는 동안에 당신은 “소스 코드의 이 라인은 seg fault를 발생시키지 않았기 때문에 틀림없이 괜찮을 것이다”와 같이 이야기 할 수 없다는 것입니다.

In other words, during your debugging session, you cannot say something like, “This line of source code must be okay, since it didn’t cause a seg fault.”


Details on the role of the page table

아래 설명에 나오는 페이지의 크기는 4,096 bytes 라고 가정하자

virtual page 0 : 0 ~ 4,095 bytes
virtual page 1 : 4,096 ~ 8,191 bytes

Keep the virtual address space in Table 4-1 in mind, and continue to assume that the page size is 4,096 bytes. Then virtual page 0 comprises bytes 0 though 4,095 of the virtual address space, page 1 comprises bytes 4,096 through 8,191, and so on.


위에서 언급했듯이, 우리가 프로그램을 실행시킬때, OS는 프로그램 코드를 실행시키는 프로세스의 가상 메모리를 관리하기 위해 사용하는 페이지 테이블을 생성합니다. 프로세스가 실행 중이라면 언제든지, 하드웨어의 page table register는 페이지 테이블을 가리키고 있을겁니다.

As mentioned, when we run a program, the OS creates a page table that it uses to manage the virtual memory of the process that executes the program code. Whenever that process runs, the hardware’s page table register will point to that table.


개념적으로 이야기하자면, 프로세스 가상 주소 공간의 각각의 페이지는 페이지 테이블 안의 entry를 갖고 있습니다. 이 페이지 테이블 entry는 페이지와 관련된 다양한 정보들을 저장하고 있습니다. 이러한 정보들 중에서 seg faults와 관련된 데이터는 페이지에 대한 접근 권한입니다. 이는 read, write, execute와 같은 파일 접근 권한과 유사합니다. 예를 들어 페이지 3번에 대한 페이지 테이블 entry는 여러분의 프로세스가 그 페이지로부터 데이터를 읽을 권한이 있는지, 그 페이지에 데이터를 쓸 권한이 있는지, 그 페이지에 지시어를 실행할 권한이 있는지 가리킬 것입니다.

Conceptually speaking, each page of the virtual address space of the process has an entry in the page table. This page table entry stores various pieces of information related to the page. The data of interest in relation to seg faults are the access permissions for the page, which are similar to file access permissions: read, write, and execute. For example, the page table entry for page 3 will indicate whether your process has the right to read data from that page, the right to write data to it, and the right to execute instructions on it.


프로그램의 실행 과정에서, 프로그램의 실행에 의해 생성되는 주소들은 가상의 값일 것입니다. 프로그램이 y라고하는 특정한 가상 주소를 갖는 메모리에 접근하려고 할때, 하드웨어는 이 가상 주소 y를 가상 페이지 번호 v로 변환할 것입니다. 여기서 가상 페이지 번호인 v는 가상 주소 y를 4,096으로 나눈 것과 같습니다. 그리고나서 하드웨어는 페이지 테이블내에서 entry값인 v가 수행하고자 하는 연산과 일치하는 권한을 가지고있는지 여부를 확인할 것입니다. 만약 일치한다면, 하드웨어는 이 테이블 entry로부터 실제 물리적 메모리 공간의 페이지 번호를 얻어낼 것이며 요청된 메모리 연산을 수행할 것입니다. 하지만 만약 테이블 entry가 요청한 연산에 대한 적절한 권한이 없을때에는 내부적인 인터럽트를 실행할 것입니다. 이 인터럽트는 OS의 에러 핸들링 루틴으로 jump하도록 해줍니다. OS는 일반적으로 메모리 접근 위반을 알려주고 프로그램의 실행을 중단시킵니다.

During the execution of the program, the addresses it generates will be virtual. When the program attempts to access memory at a certain virtual address, say y, the hardware will convert that to a virtual page number v, which equals y divided by 4,096. The hardware will then check entry v in the page table to see whether the permissions for the page match the operation to be performed. If they do match, the hardware will get the desired location’s actual physical page number from this table entry and then carry out the requested memory operation. But if the table entry shows that the requested operation does not have the proper permission, the hardware will execute an internal interrupt. This will cause a jump to the OS’s error-handling routine. The OS will normally then announce a memory access violation and discontinue execution of the program.


A slight memory-access bug might not cause a seg fault

  • cpp
1
2
3
4
5
6
7
8
int q[200];
main()
{
int i;
for (i = 0; i < 2000; i++) {
q[i] = i;
}
}

거의 대부분의 경우 실행 시간에 seg fault가 발생할 것입니다. 하지만 에러가 발생하는 시점이 당신을 놀라게 할 것입니다. 에러는 자연스러운 시점에 발생하지 않을 가능성이 있는데, 다시 말해, i가 200인 경우가 아닌 그 이후 시점에 발생할 수 있다는 것입니다.

At execution time, a seg fault is quite likely to occur. However, the timing of the error may surprise you. The error is not likely to appear at the “natural” time, that is, when i = 200; rather, it is likely to happen much later than that.


이 현상을 재현하기 위해 우리는 이 프로그램을 변수의 주소를 검색하기에 편리한 GDB를 통해 실행시켰습니다. 이를 통해 seg fault는 i가 200이 아닌 728(테스트 환경에 따라 i의 값은 달라질 수 있음)에서 발생했다는 것을 확인할 수 있었습니다. 왜 이러한 현상이 발생하는지 살펴봅시다.

To illustrate this, we ran this program on a Linux PC under GDB, in order to conveniently query addresses of variables. It turned out that the seg fault occurred not at i = 200, but at i = 728. Let’s see why.


GDB에 질의 결과, 우리는 배열 q(q[])의 주소값이 0x80497bf로 끝난다는 것을 확인했습니다. 즉, q[199]의 마지막 byte가 해당 메모리 주소에 존재한다는 것입니다. 페이지 크기는 4,096 byte에 32-bit의 word 크기를 갖는 장치라고 고려해본다면, 가상 주소 공간은 20-bit의 페이지 번호와 12-bit의 오프셋으로 나누어집니다. 우리 프로그램의 경우, 배열 q[]는 가상 페이지 번호 0x8049 = 32841, 오프셋 값 0x7bf = 1983이 됩니다. 그래서 q가 할당된 메모리의 페이지 위에 여전히 2,112 byte의 공간이 남아있게 됩니다. 이 공간은 integer 변수 528개를 저장할 수 있으며, 우리 프로그램은 그것을 마치 q의 요소들이 포함된 공간으로 처리하게 됩니다(q[200] ~ q[727]).

From queries to GDB we found that the array q[] ended at address 0x80497bf; that is, the last byte of q[199] was at that memory location. Taking into account the Intel page size of 4,096 bytes and the 32-bit word size of this machine, a virtual address breaks down into a 20-bit page number and a 12-bit offset. In our case, q[] ended in virtual page number 0x8049 = 32841, offset 0x7bf = 1983. So there were still 4,096 − 1,984 = 2,112 bytes on the page of memory on which q was allocated. That space can hold 2112 / 4 = 528 integer variables (since each is 4 bytes wide on the machine used here), and our code treated it as if it contained elements of q at “positions” 200 through 727.


물론 q[] 배열의 이러한 요소(q[200] ~ q[727])들은 실제로 존재하지 않지만, 컴파일러는 불만을 제기하지 않습니다. 여전히 해당 페이지는 쓰기 권한을 가지고 있기 떄문에 하드웨어 또한 불만을 제기하지 않습니다. 오로지 i가 728이 되는 시점에 q[i]는 다른 페이지를 가리키게 됩니다. 바로 이 경우에, 해당 페이지에는 쓰기 권한이 없게되고 가상 메모리 하드웨어는 이를 감지하여 seg fault를 발생시킵니다.

Those elements of q[] don’t exist, of course, but the compiler did not complain. Neither did the hardware, since the writes were still being performed to a page for which we certainly had write permission. Only when i became 728 did q[i] refer to an address on a different page. In this case, it was a page for which we didn’t have write (or any other) permission; the virtual memory hardware detected this and triggered a seg fault.


교훈: 이전에 언급했듯이, seg fault가 발생하지 않았다는 것이 메모리 연산에 오류가 없다는 것은 아닙니다.

The moral: As stated earlier, we can’t conclude from the absence of a seg fault that a memory operation is correct.



해당 게시글에서 발생한 오탈자나 잘못된 내용에 대한 정정 댓글 격하게 환영합니다😎

Reference