preface

Do you think it’s possible to create your own MemoryGraph tool? . The Malloc Stack Logging tool can be manually turned on or off at run time, and log files can be retrieved and analyzed.

In the spirit of learning, implement a simple offline analysis of the Malloc Stack Logging logs and record the problems encountered. Personal level is limited, the code is for reference only.

code

The code address

In addition to the project code, the MallocStackLogging file is a system MallocStackLogging dynamic library that I extracted from my jailbroken iPhone7 and can drag into Hopper analysis. Libmalloc-166.251.2 is my annotated source code.

The target

To parse a log file offline, you need to know where the log file is and in what format is the log file data saved

Where are the log files?

As mentioned in the article, the log files are stored in the TMP directory of the sandbox. Still, do some research for yourself.

As tested, no log was generated when Live Allocations Only was selected. (Because that’s what the libmalloc source code says). If All Allocation and Free History is selected,

Is the log file address just given?

How are log files stored?

extern boolean_t turn_on_stack_logging(stack_logging_mode_type mode);
extern void turn_off_stack_logging(void);
Copy the code

I decided to start with two known switching methods. Through source libmalloc learned – 317.140.5 libmalloc are called MallocStackLogging. Framework method

Load the MallocStackLogging library and register it with libmalloc boolean_t malloc_register_stack_logger(void);

MALLOC_EXPORT boolean_t turn_on_stack_logging(stack_logging_mode_type mode) { malloc_register_stack_logger(); if (! msl.dylib) { return false; } boolean_t (*msl_turn_on_stack_logging) (stack_logging_mode_type mode); msl_turn_on_stack_logging = _dlsym(msl.dylib, "msl_turn_on_stack_logging"); if (! msl_turn_on_stack_logging) { return false; } return msl_turn_on_stack_logging(mode); }Copy the code
MALLOC_EXPORT boolean_t malloc_register_stack_logger(void) { if (msl.dylib ! = NULL) { return true; } void *dylib = _dlopen("/System/Library/PrivateFrameworks/MallocStackLogging.framework/MallocStackLogging", RTLD_GLOBAL); if (dylib == NULL) { return false; } os_once(&_register_msl_dylib_pred, dylib, register_msl_dylib); if (! msl.dylib) { malloc_report(ASL_LEVEL_WARNING, "failed to load MallocStackLogging.framework\n"); } return msl.dylib == dylib; }Copy the code

No matter how, first get MallocStackLogging. Framework. Because MallocStackLogging. The framework is the private library, so I need from prison break phone parsing out dynamic library cache (dyld_shared_cache_arm64). Prison break, analytical steps no longer go into here, there are a lot of this information, get MallocStackLogging. Folder has a framework in the code, interested friends, can take to see.

Throw it in the Hopper.

The whole library is actually not very big, and there are not many symbols. I saw some interesting symbols like _create_log_file.

What’s the next step? You’re not going to actually analyze the assembly, are you?

Do a search on Github

I did a random function search and see what I found

The source code is available in earlier versions of the libmalloc library.

The following analysis is based on libmalloc-166.251.2. Libmalloc-283 no longer has this code.

So, I have a bold idea:

You didn’t do anything, just move the code to MallocStackLogging. The framework of right? (It’s actually changed)

Libmalloc-166.251.2 source analysis

I also put the annotated source code libmalloc-166.251.2 in the code file, with many of my comments and poor translations. If interested in the source code of friends, you can read. I hope it helps.

Like a comment like this.

Turn_on_stack_logging function

turn_on_stack_logging __prepare_to_log_stacks(false); // Initialize pre_write_buffers __create_uniquing_table// Create a hash table for storing the memory allocation stack (uniquing_table create_log_file) // create a log file Index_file_descriptor // Static variable log file handle __stack_log_file_path__// global variable log file address malloc_logger = __disk_stack_logging_log_stack; __syscall_logger = __disk_stack_logging_log_stack;Copy the code

Indentation indicates the invocation relationship. Can see MallocStackLogging. The framework is also USES two hook function malloc_logger and __syscall_logger memory allocation of log information. At the core is the __create_uniquing_table function, which is used to create the backtrace_uniquing_table hash table that holds allocation stack information.

Suppose we have two function call stacks like this:

funcC  funcD
funcB  funcB
funcA  funcA
Copy the code

Their storage in the hash table looks like this: Talent!

And just to make it easier to understand, I drew it like this. Parent stores the hash value of the parent node. For more details, see the enter_frames_in_table function. I put a lot of notes in it.

In fact, we only need to know one thing, the hash table backtrace_uniquing_table holds stack information. The final stack_id returned is the hash value of the uppermost call instruction (e.g. FuncC funcD) and is also a subscript. With a stack_id, I can get funcC funcB funcA in succession.

A stack_ID corresponds to a stack list information.

I also see it in the __prepare_to_log_stacks function

This also explains the problem that no log files were generated at Live Allocations Only above.

Turn_off_stack_logging function

turn_off_stack_logging
	malloc_logger = NULL;
	__syscall_logger = NULL;
	stack_logging_enable_logging = 0;
Copy the code

There’s nothing left to say about this function, so I’m going to empty the two ticks.

__disk_stack_logging_log_stack function

__disk_stack_logging_log_stack __prepare_to_log_stacks(false); // If turn_on_stack_logging is initialized, __prepare_to_log_STACKs_stage2 // Mainly deleting unwanted log files reap_orphaned_log_files(getpid(), NULL)// Delete unnecessary log files reap_log_FILES_in_hierarchy // Delete unnecessary log files open_log_file_at_path(pathname, streams); Delete_logging_file deletes the log __enter_STACK_into_table_while_locked thread_stack_PCS to get a list of stack instructions for the current thread Enter_frames_in_table The currently allocated stack instruction array is saved to the hash table uniquing_table __expand_UNIQUing_table fails to stack into the table Uniquing_table // VM_allocate additional logic in thin mode -- start radix_tree_create Allocate a page of virtual memory radix_tree_init radix_tree_insert // in thin mode, vm_allocate allocates additional logic -- end flush_data(); // Write to the log fileCopy the code

Both hooks go to __disk_stack_logging_log_stack, where the core logic for loading stack information into tables and writing files is located. First, the stack information is stored in the hash table, and enter_frames_in_table returns stack_ID upon successful entry. How about creating your own MemoryGraph tool? It says:

Looking at the source code, it turns out that instead of just making a map of stackID and frames, the system uses a tree to store stacks. And it’s in memory. This reduces memory footprint and creates a mapping between stackids and trees, making lookup very fast! It’s amazing!

However, according to the result I looked at the source code. The tree operation is only done in thin mode, and the tree entry is stack_ID and allocated memory address. I don’t know if it’s because I was looking at an older version of the libmalloc library.

if (stack_logging_mode_lite_or_vmlite && (type_flags & stack_logging_type_vm_allocate)) { if (pre_write_buffers) { // If (! pre_write_buffers->vm_stackid_table) { pre_write_buffers->vm_stackid_table = radix_tree_create(); pre_write_buffers->vm_stackid_table_size = radix_tree_size(pre_write_buffers->vm_stackid_table); } if (pre_write_buffers->vm_stackid_table) { uint64_t address = return_val; radix_tree_insert(&pre_write_buffers->vm_stackid_table, trunc_page(address), round_page(address+size) - trunc_page(address), uniqueStackIdentifier); pre_write_buffers->vm_stackid_table_size = radix_tree_size(pre_write_buffers->vm_stackid_table); } } goto out; }Copy the code

The flush_data function then writes the data to the log file.

What data? stack_logging_index_event

typedef struct {
	uintptr_t argument;
	uintptr_t address;
	uint64_t offset_and_flags; // top 8 bits are actually the flags!
} stack_logging_index_event;
Copy the code

The argument argument is the size of memory. Offset_and_flags is a bit more complicated. Through a bunch of macro bit operations.

(lower 48 of the 16 0 _stack_id) | (type_flags low 8 _56 a 0) | (type_flags 24 to 32 _48 0) = Type_flags lower 8 bits _type_flags lower 48 bits _stack_id = total 64 bitsCopy the code

The 64 bits of offset_AND_FLAGS hold stack_ID and type_FLAGS and user_tag

Source summary

So far, I know that the log files hold stack_logging_index_event structures. After parsing the log file and getting stack_logging_index_event, I can get the stack_id corresponding to address, and then check the stack hash table with stack_ID to get the stack information for allocating or freeing the memory.

Parsing log files

The next step is to write code.

NSLog(@" get the log file: %@",filePath); const char *path = [filePath cStringUsingEncoding:4]; FILE *fp = fopen(path, "r"); //❌ error 1 char bufferSpace[4096]; size_t read_count = 0; size_t read_size = sizeof(my_stack_logging_index_event); size_t number_slots = (size_t)(4096 / read_size); if (fp ! = NULL) { do { read_count = fread(bufferSpace, read_size, number_slots, fp); if (read_count > 0) { my_stack_logging_index_event *target_64_index = (my_stack_logging_index_event *)bufferSpace; for (int i = 0; i < read_count; i++) { my_stack_logging_index_event index_event = target_64_index[i]; my_mach_stack_logging_record_t pass_record; pass_record.address = STACK_LOGGING_DISGUISE(index_event.address); pass_record.argument = target_64_index[i].argument; pass_record.stack_identifier = STACK_LOGGING_OFFSET(index_event.offset_and_flags); pass_record.type_flags = STACK_LOGGING_FLAGS_AND_USER_TAG(index_event.offset_and_flags); NSString *type = typeString(pass_record.type_flags); NSLog(@"%@ size:%llu stackid:0x%llx address:%p",type,pass_record.argument,pass_record.stack_identifier,(void *)pass_record.address); } } } while (read_count > 0); fclose(fp); }Copy the code

The code is simple, read the log file, parse.

The data is not right in any way.

Let’s take a look at what the data looks like when parsed using the system API.

extern kern_return_t __mach_stack_logging_enumerate_records(task_t task, mach_vm_address_t address, void enumerator(my_mach_stack_logging_record_t, void *), void *context); void enumerate_records_hander(my_mach_stack_logging_record_t record, void * context) { NSString *type = typeString(record.type_flags); NSLog(@"%@ size:%llu stackid:0x%llx address:%p",type,record.argument,record.stack_identifier,(void *)record.address); } - (void)test_enumerate_records { if (! _isOpen) {NSLog(@"❎ has not turned on the log switch "); return; } __mach_stack_logging_enumerate_records(mach_task_self(), NULL, enumerate_records_hander, NULL); }Copy the code

As a result,

This number, it looks a lot more normal.

What’s the problem? I tried to find the header file of the private library to understand the real structure without success. At this point, I thought, “How about a MemoryGraph tool of my own?” As mentioned in this article, the contents of the log file are grouped into four numbers. This is inconsistent with the result I see the source code. But give it a try. Actually, the first time you parse it, there’s a detail. The first number looks a little bit normal.

Modify the structure:

//❌ second error typedef struct {uint64_t argument; uint64_t address; uint64_t offset_and_flags; uint64_t what; } wrong_stack_logging_index_event64;Copy the code

Results:

It looks like address and size are correct. Stack_id and type_flags are incorrect.

Here, I would also like to think 🤔, under what circumstances would a structure need to be modified? 2. The type of the member is not enough, we need to change the structure……

So, will offset_and_flags be removed? Then you don’t need so many bit operations.

Verify:



On the left is the first data I parsed myself. On the right is the data parsed using the system API.

Note the offset_AND_flags and WHAT values on the left

So the guess is correct. I wonder if any friends will confuse the high two. Because stack_logging_type and user_tag are stored together. See the VM_GET_FLAGS_ALIAS macro in < Mach /vm_statistics.h>, etc. (this part is my understanding).

Ok, modify the structure again.

Uint64_t argument; uint64_t address; uint64_t offset; uint64_t flags; } test_stack_logging_index_event64;Copy the code

This time, there is no mapping, and the result is consistent with the system API parsing.

Get stack_id. I can look up the hash table.

Next, many system library apis are used. Both can be found in the stack_logging.h file in the libmalloc-317.140.5 library.

Struct backtrace_uniquing_table *table = __mach_stack_logging_copy_uniquing_table(mach_task_self); struct backtrace_uniquing_table *table = __mach_stack_logging_copy_uniquing_table(mach_task_self) if (table ! = NULL) {mach_vm_address_t frames[MAX_FRAMES]; uint32_t frames_count; Kern_return_t ret = __mach_stack_logging_uniquing_table_read_stack(table, last_person_record.stack_identifier, frames, &frames_count, MAX_FRAMES); if (ret == KERN_SUCCESS) { if (frames_count > 0) { NSLog(@"number of frames returned from __mach_stack_logging_get_frames = %u\n", frames_count); NSLog(@" The allocation stack for the person object is as follows :"); for (int i = 0; i < frames_count; i++) { vm_address_t addr = frames[i]; Dl_info info; dladdr((void *)addr, &info); NSLog(@"--- %s",info.dli_sname); }}else {NSLog(@"__mach_stack_logging_uniquing_table_read_stack call failed ❎"); } // release hash table __mach_stack_logging_UNIQUing_table_release (table); }Copy the code

I tried going to the hash table to find the allocation stack for a Person object I was observing:

At this point, the logic to parse the log file and query the hash table is clear.

As for offline parsing, methods for serializing and deserializing hash tables are already provided in the system API.

(Apple: We guessed your little trick)

We just need to write it into a file. Next time you can just grab a hash file, a log file, and parse it. There is code in the project. To simplify the logic (lazy), the code for writing and reading files is very crudely written. For reference only.

Some personal reflections

In fact, in the process of looking at the source code, I have been thinking about the use of this function scenario. Use the system open two hooks, to record the memory allocation release stack information function. We’ve already seen this in matrix-ios and OOMDetector libraries. These two libraries may also refer to official implementations. Even the __syscall_logger hook is written in the Matrix-ios wiki to be audit risk.

Can I really collect logs from users’ phones with this feature?

Even if I could. How can such a large number of log files be uploaded to our server? Users’ mobile disk space is at a premium. Otherwise, iOS would not have replaced memory swapping with memory compression. Does that mean it can only be used during development?

A large number of hash table operations in libmalloc-317.140.5 stack_logging.h are also marked as out of date.

The last

Libmalloc-166.251.2 libmalloc-166.251.2 libmalloc-166.251.2 And we are really used MallocStackLogging. Framework. However, there is still much to learn from the source code. Finally, thanks big guy’s article, Thanksgiving source code.