Trace Dynamic Memory Allocations
Overview
In this post we’ll cover two macOS applications, part of Xcode Developer Tools, which can be extremely useful to track memory allocations and leaks - malloc_history
and leaks
.
malloc_history
malloc_history
provides a detailed account of every memory allocation that occurred in the process, including allocations by dyld
. malloc_history
relies on information provided by the standard malloc
library when malloc
stack logging has been enabled for the target process (for example by setting the MallocStackLogging
environment variable).
To show all allocations currently live in a process, use –allBySize
or –allByCount
flags:
$ MallocStackLogging=1 open /Applications/Safari.app
$ ps axu | grep Safari.app
m 4828 0.0 3.0 3788332 61888 ?? S 5:56pm 0:02.23 /Applications/Safari.app/Contents/MacOS/Safari
$ malloc_history 4828 -allBySize
alloc_history Report Version: 2.0
Process: Safari [4828]
Path: /Applications/Safari.app/Contents/MacOS/Safari
Load Address: 0x10229d000
Identifier: com.apple.Safari
Version: 11.0.2 (12604.4.7.1.4)
Build Info: WebBrowser-7604004007001004~1
Code Type: X86-64
Parent Process: ??? [1]
Date/Time: 2018-03-20 19:56:27.939 +0000
Launch Time: 2018-03-20 17:56:02.600 +0000
OS Version: Mac OS X 10.12.6 (16G1114)
Report Version: 7
Analysis Tool: /usr/bin/malloc_history
----
2 calls for 1003520 bytes: thread_700005856000 | start_wqthread | _pthread_wqthread | _dispatch_worker_thread3 | _dispatch_root_queue_drain | _dispatch_queue_override_invoke | _dispatch_queue_invoke | _dispatch_queue_serial_drain | _dispatch_client_callout | _dispatch_call_block_and_release | __63-[ClosedTabOrWindowStateManager performDelayedLaunchOperations]_block_invoke | -[ClosedTabOrWindowStateManager _loadRecentlyClosedTabsOrWindowsFromDisk] | -[BrowserTabPersistentState initWithDictionaryRepresentation:encryptionProvider:] | -[KeychainEncryptionProvider decryptData:] | +[NSMutableData(NSMutableData) dataWithLength:] | -[NSConcreteMutableData initWithLength:]
[..]
To include deallocations as well, use -allEvents
flag.
Another interesting usage is with the -callTree
option, which generates a call tree of the backtraces of malloc calls
for all live allocations in the target process:
$ malloc_history 4828 -callTree -showContent
Process: Safari [4828]
Path: /Applications/Safari.app/Contents/MacOS/Safari
Load Address: 0x10229d000
Identifier: com.apple.Safari
Version: 11.0.2 (12604.4.7.1.4)
Build Info: WebBrowser-7604004007001004~1
Code Type: X86-64
Parent Process: ??? [1]
Date/Time: 2018-03-20 20:01:48.869 +0000
Launch Time: 2018-03-20 17:56:02.600 +0000
OS Version: Mac OS X 10.12.6 (16G1114)
Report Version: 7
Analysis Tool: /usr/bin/malloc_history
----
Call graph:
76221 (13.0M) << TOTAL >>
57934 (7.82M) Thread_ec1f13c1
+ 56356 (7.50M) start (in libdyld.dylib) + 1 [0x7fffe32d4235]
+ ! 45992 (6.13M) NSApplicationMain (in AppKit) + 1237 [0x7fffcb604e0e]
+ ! : 41313 (4.84M) -[NSApplication run] (in AppKit) + 926 [0x7fffcb63a3db]
+ ! : | 41313 (4.84M) -[BrowserApplication nextEventMatchingMask:untilDate:inMode:dequeue:] (in Safari) + 252 [0x10235c686]
+ ! : | 41306 (4.84M) -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] (in AppKit) + 2796 [0x7fffcbdc17ee]
+ ! : | + 23726 (2.89M) _DPSNextEvent (in AppKit) + 1833 [0x7fffcb645d1d]
[..]
leaks
The leaks (1)
tool walks the process heap to detect suspected memory leaks. It looks for pointers which have been allocated but not freed. For example, taking the same Safari instance from above, we can see there are no leaks. Nice!
$ leaks 4828
Process: Safari [4828]
Path: /Applications/Safari.app/Contents/MacOS/Safari
Load Address: 0x10229d000
Identifier: com.apple.Safari
Version: 11.0.2 (12604.4.7.1.4)
Build Info: WebBrowser-7604004007001004~1
Code Type: X86-64
Parent Process: ??? [1]
Date/Time: 2018-03-20 20:05:07.732 +0000
Launch Time: 2018-03-20 17:56:02.600 +0000
OS Version: Mac OS X 10.12.6 (16G1114)
Report Version: 7
Analysis Tool: /usr/bin/leaks
----
leaks Report Version: 2.0
Process 4828: 76238 nodes malloced for 13288 KB
Process 4828: 0 leaks for 0 total leaked bytes.
Practice
To make sure it works as expected, let’s build a simple program to check:
#include <stdio.h>
#include <stdlib.h>
void doLeak() {
char *c = malloc(256);
}
int main(int argc, char *argv[]) {
int i = 0;
while(i++ < 3)
doLeak();
getchar(); // Wait for key press
return 0;
}
Compile, launch and verify:
$ clang leaky.c -o leaky
$ ./leaky
$ ps axu | grep leaky
m 4976 0.0 0.0 2432780 636 s002 S+ 8:13pm 0:00.00 ./leaky
$ leaks 4976
Process: leaky [4976]
Path: /Users/m/leaky
Load Address: 0x103ea0000
Identifier: leaky
Version: ???
Code Type: X86-64
Parent Process: zsh [4329]
Date/Time: 2018-03-20 20:13:58.547 +0000
Launch Time: 2018-03-20 20:13:27.793 +0000
OS Version: Mac OS X 10.12.6 (16G1114)
Report Version: 7
Analysis Tool: /usr/bin/leaks
----
leaks Report Version: 2.0
Process 4976: 157 nodes malloced for 17 KB
Process 4976: 3 leaks for 768 total leaked bytes.
Leak: 0x7fec8b4025d0 size=256 zone: DefaultMallocZone_0x103ea5000
0x00000000 0xd0000000 0x00000000 0xd0000000 ................
0xec080010 0x00007fff 0xe29e308f 0x00007fff .........0......
0xec0870c8 0x00007fff 0xe29e3095 0x00007fff .p.......0......
0xec087168 0x00007fff 0xe29fac2b 0x00007fff hq......+.......
0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00000000 0x00000000 0x00000000 0x00000000 ................
0x00000000 0x00000000 0x00000000 0x00000000 ................
[..]
All the allocated and not freed memory locations (256*3=768
bytes) were correctly detected!