본문 바로가기

Research/Linux

glibc malloc understanding




https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/

예전에 읽었던 자료 정리 삼아.


https://sploitfun.wordpress.com/2015/03/04/heap-overflow-using-malloc-maleficarum/




ptmalloc2 는 기존에 사용 중이었던 dlmalloc 을 fork 한 함수이며 멀티스레드를 지원함.

이는 freelist 자료구조가 교체의 원인이 되었는데 (아마 bins 를 얘기하는 듯) 기존의 dlmalloc 의 경우 freelist 자료구조가 모든 쓰레드에서 접근가능했고, 두개의 스레드에서 동시에 malloc 을 할 경우에 하나의 스레드만이 크리티컬 섹션에 진입할 수 있었기 때문에 퍼포먼스가 하락하는 문제가 있었다. ptmalloc2 를 사용하면 스레드에서 메모리할당 요청을 할 때 스레드별로 분리된 힙 세그만트를 할당해 주며 그에 따라 freelist 자료구조 또한 힙과 같이 분리된 형태를 띄게 된다. 이렇게 분리되어 구성된 힙과 freelist 자료구조 형태를 per thread arena 라고 한다.



** thread arena 와 관련된 소스 코드를 예제로 듦. 코드 설명에 따르면 malloc 하기 이전에는 heap segment 나 스레드별 스택이 생성되지 않는다고 하지만 이는 실제 환경마다 다르며 우분투의 경우 malloc 유무에 관계없이 main heap 은 프로그램 실행 시에 할당하는 걸로 보임. FreeBSD 에서는 할당을 안하고.


FREEBSD 에서의 할당과정


[0050f424] uname({sys="Linux", node="null2root-centos6", ...}) = 0

[0050f424] fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0

[0050f424] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77dc000

[0050f424] write(1, "Welcome to per thread arena exam"..., 43Welcome to per thread arena example::12393

) = 43

[0050f424] write(1, "Before malloc in main thread\n", 29Before malloc in main thread

) = 29

[0050f424] fstat64(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0

[0050f424] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77db000

[0050f424] read(0, // first getchar()

"\n", 1024)          = 1

[0050f424] brk(0)                       = 0x8b92000

[0050f424] brk(0x8bb3000)               = 0x8bb3000

[0050f424] write(1, "After malloc and before free in "..., 44After malloc and before free in main thread

) = 44

[0050f424] read(0, // second getchar()



우분투에서의 할당과정


[b7fdac31] uname({sysname="Linux", nodename="ubuntu", ...}) = 0

[b7fdac31] fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 19), ...}) = 0

[b7fdac31] brk(NULL)                    = 0x804b000

[b7fdac31] brk(0x806c000)               = 0x806c000

[b7fdac31] write(1, "Welcome to per thread arena exam"..., 43Welcome to per thread arena example::14844

) = 43

[b7fdac31] write(1, "Before malloc in main thread\n", 29Before malloc in main thread

) = 29

[b7fdac31] fstat64(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 19), ...}) = 0

[b7fdac31] read(0, // frist getchar()

"\n", 1024)          = 1

[b7fdac31] write(1, "After malloc and before free in "..., 44After malloc and before free in main thread

) = 44

[b7fdac31] read(0, // second getchar()




힙 세그먼트는 데이터 세그먼트(rw-p) 바로 아래에 붙어 할당되며, 이는 곧 brk syscall 과 같은 'increasing program break' 과정에 의해 할당됨을 알 수 있다. 머 그냥 메모리 확장 관련한 의미인듯. 코드상에선 1000바이트를 alloc 해도 

실제로 alloc 되는것은 꽤나 큰 사이즈다. 132KB 라고 문서에 써있는데 실제로 우분투 / 프비 환경에서도 132 KB 가 할당되었다. 값을 바꿔도 그렇고. 고정값인가..

힙 메모리에 인접한 영역을 아레나라고 하고, 메인스레드에서 생성된 아레나이기 때문에 메인아레나라고 부른다. 메모리 할당요청이 오면 더 이상 할당할 공간이 없을 때까지(out of free space) 이 아레나를 사용한다. 이 공간을 넘어서게 되면 'increasing program break location' 작업을 통해 공간 확장이 가능하다. 확장되고 나면 최상위 청크(top chunk)의 사이즈 값은 공간이 늘어난 만큼 증가한다. 뭐 이건 당연. 비슷하게 free space 가 너무 많은 경우 공간을 줄일 수도 있다.


** maps 로 보는 공간에서 [heap] 과 라이브러리 so 사이의 영역이 free space 인 것으로 추측되며, 힙을 새로 할당할 때마다 이 공간의 사이즈를 줄여가며 할당하는 것이 근거이다.



이후 main 에서의 free 작업을 하더라도, OS 는 메모리 공간을 바로 릴리즈하지는 않는다. 1000바이트로 할당되었던 메모리공간은 glibc malloc 라이브러리에 의해서만 릴리즈되는데, 이 malloc 라이브러리는 free 된 블록을 메인 아레나의 bin (freedata 자료구조) 에다가 추가한다. 그리고 사용자가 메모리 요청을 하면 glibc malloc 은 커널로부터 새로운 힙 메모리를 할당하는 게 아니라 bin 에서 free block 을 찾는 작업을 먼저 한다. free block 이 없으면 그때 커널에 요청하지.


thread1 스레드가 생성되면 힙 세그먼트는 없지만 per thread stack 은 생성된다

** [stack] 이라 적혀있는 부분을 아직 확인하지 못함. 힙에다 할당하고 남은 나머지 데이터 세그먼트의 영역이 전부 스택으로 사용되는 건가 ?


스레드 1에서 malloc 한 이후, thread1 의 힙세그먼트가 생성된걸 확인할수 있다. maps 를 볼 때 메인 힙 세그먼트 바로 아래에 위치하고 있으며 132KB 의 크기를 가지고 있고, 또한 이 힙 메모리가 메인스레드와는 다르게 mmap syscall 을 사용해서 할당되었다는 것을 확인할 수 있다(brk()가 아님). 메인 스레드에서처럼 1000바이트를 할당 요청했지만 힙 메로리는 1MB(0x1000 byte) 를 프로세스 주소 공간에 매핑한다(0xb750000-0xb760000). 이 1MB 중 RW 퍼미션을 가진 132KB 만큼의 영역이 이 스레드의 힙 메모리로 사용된다. 이 부분을 스레드 아레나라고 부른다.





08b92000-08bb3000 rw-p 00000000 00:00 0          [heap] // 메인 아레나


b6c00000-b6c21000 rw-p 00000000 00:00 0           } 스레드가 생성될 때 만들어진 1MB

b6c21000-b6d00000 ---p 00000000 00:00 0           } 이 중 rw 퍼미션을 가진 128KB 가 스레드 아레나(힙 세그먼트)


b6dcb000-b6dcc000 ---p 00000000 00:00 0

b6dcc000-b77cd000 rw-p 00000000 00:00 0

b77db000-b77de000 rw-p 00000000 00:00 0


bfd7c000-bfd91000 rw-p 00000000 00:00 0          [stack] // 메인 함수의 스택



사용자가 128KB 이상의 메모리할당 요청하면(아레나 크기 이상을 요청하는 경우) sbrk 가 아닌 mmap 시스템 콜을 통해 메모리가 할당되며 이는 메인 아레나나 스레드 아레나와는 관련없이 동일하게 적용된다.


 스레드의 메모리가 반환된 이후에도 OS 에 의한 릴리즈는 일어나지 않으며, 마찬가지로 thread arenas bin 에 해당 영역이 기록되게 된다.




메인 스레드는 메인 아레나 하나를 갖고, 스레드 하나는 하나의 스레드 아레나를 갖는다는 것을 확인했다. 그럼 스레드의 개수에 상관없이 스레드와 아레나는 1:1 매핑을 하는가(스레드당 아레나 하나). 가끔 정신나간 어플리케이션들은 코어의 갯수보다 더 많은 수의 스레드를 포함하고 있는데, 이 경우 스레드 하나당 아레나를 가지는 것은 자원을 많이 먹을 뿐더러 쓸모가 없다. 이런 이유로 어플리케이션은 코어의 개수에 비례하여 아레나의 갯수 제한을 두고 있다.

32비트의 경우 코어 *2, 64비트의 경우 코어 *8


** 이는 아마 스레드 개수 최적화와도 관련이 있는 것으로 보이며, 예전에 코딩하다 봤던 최적화 된 스레드의 개수가 코어수*2+1 이라는 것과도 비슷해 보인다





멀티스레드(main 1 + user thread 3) 어플리케이션이 1코어 시스템에서 돌아간다고 가정해 보자. 일단 아레나 개수 제한에서 걸리겠지. 이런 경우 glibc malloc 은 모든 스레드가 접근 및 공유 가능한 멀티블 아레나를 만들도록 한다.

본문에서 아래로는 이 방식에 대한 이야기가 있는데 재사용의 block 관련하여 이해가 잘 되지 않는다...



그림이 좋은데, 우선 heap_info / malloc_state / chunk 가 있다. 

heap_info : 하나의 힙은 하나의 헤더를 가져야 한다. 하나의 스레드 아레나는 여러 개의 힙을 가질 수 있는데, 모든 스레드 아레나가 하나의 힙을 가지고 있어도 만약 메모리 할당이 힙의 크기를 넘어가는 경우 연속적이지 않은 위치에 새로운 힙이 할당되기도 하기 때문이다 (?). 메인 아레나의 경우 여러 개의 힙을 가질수 없기 때문에 heap_info 가 없다.  




정리하다 보니 한글로 잘 번역된 자료가 있었구만.  힙 구조에 대한 설명은 아래 링크를 통해 확인하는 것이 좋겠다.

http://mashirogod.dothome.co.kr/index.php/2016/05/11/glibc-malloc-1/




'Research > Linux' 카테고리의 다른 글

[번역] mach-O 와 ELF 의 링킹 과정에 대해  (0) 2018.02.19
docker template  (0) 2016.11.05
동적 메모리 관련  (0) 2016.09.06
directly run python code using vim  (0) 2016.06.08
When sidebar(unity) disappeared in Ubuntu  (0) 2016.05.31