[iOS 연구 노트]——와일드 포인터와 좀비 객체 위치 지정에 대해 이야기

[iOS 연구 노트]——와일드 포인터와 좀비 객체 위치 지정에 대해 이야기

1. 예외로 시작

iOS 프로젝트의 개발에서 다소간 충돌이 발생합니다. 충돌로 인해 발생하는 대부분의 예외는 NSException 계층입니다. 이러한 예외는 OC 계층 코드 문제, 일반적으로 스택 정보 및 예외로 인해 발생합니다. 프롬프트 정보는 매우 명확합니다. 문제가 있는 코드를 직접 찾아낼 수 있어 문제 해결이 어렵지 않습니다. NSException 외에도 Crash를 유발할 수 있는 예외에는 Unix 계층 및 Mach 계층 예외가 포함됩니다.

Mach 예외는 일반적으로 낮은 수준의 커널 수준 예외입니다. 일부 낮은 수준 API를 통해 이러한 예외를 캡처할 수 있습니다. 이것은 이 기사의 내용이 아니며 여기에서 반복하지 않을 것입니다. Unix 계층은 개발자가 캐치하지 못한 Mach 예외를 말하며 해당 Unix 신호로 변환되어 오류 스레드로 전달됩니다.

iOS 프로젝트에서 온라인으로 수집하는 예외 중 EXC_BAD_ACCESS와 유사한 예외가 있는 경우 메모리 문제로 인해 생성된 와일드 포인터가 원인일 가능성이 높습니다. 이것은 또한 우리가 이 기사에서 논의할 핵심 내용입니다.

1. 와일드 포인터란 무엇입니까?

현재 우리는 iOS 프로그램을 작성할 때 메모리 관리를 위해 ARC를 주로 사용하는데, 일반적으로 메모리 관리는 크게 신경 쓸 필요가 없습니다. 하지만 그렇다고 해서 메모리 문제가 없는 것은 아닙니다. 원칙적으로 객체를 생성할 때 운영 체제를 통해 객체가 사용할 메모리 공간을 먼저 신청하고 이 메모리 공간의 주소를 포인터에 저장하여 코드에서 이 항목에 대한 편리한 참조를 제공합니다. 메모리. 그런 다음 개체가 파괴될 때 원칙적으로 두 가지 작업을 수행해야 합니다. 하나는 메모리를 반환하고 운영 체제는 메모리를 재사용하여 다른 신청자에게 할당하고 다른 하나는 코드에 포인터를 할당하는 것입니다. 빈 재활용. 이렇게 하면 프로그램이 지속 가능하고 건전하게 실행될 수 있습니다. 작업 프로세스는 다음 그림에 나와 있습니다.

하지만 인생을 살든 프로그래밍을 하든 항상 사고는 발생하기 마련이다.일반적으로 운영체제에 메모리를 적용하는 단계는 문제가 거의 없다.운영체제 자체의 안정성은 애플리케이션 프로그램의 안정성보다 훨씬 강하다. 문제는 주로 메모리가 해제될 때 발생합니다. 두 가지 종류의 문제가 있을 수 있습니다.

하나는 더 이상 필요하지 않은 객체의 포인터 변수를 직접 지우지만 운영 체제에 이 메모리를 회수하라고 지시하지 않는 것입니다. 그 후 프로그램에서 이 메모리의 주소를 저장할 장소가 없으며 이 메모리는 사용 및 회수되지 않습니다. 이 경우 이 메모리 조각이 소유되지 않은 메모리가 되어 운영체제가 이를 알지 못하기 때문에 우리가 흔히 이야기하는 메모리 누수 문제가 발생합니다. 너무 많으면 결국 메모리가 부족해지고 프로그램이 더 이상 정상적으로 실행되지 않습니다.

다른 하나는 운영 체제에 이 메모리 조각을 회수하도록 지시하고 이 메모리 조각은 실제로 회수되지만 이 주소를 저장하고 비워지지 않은 포인터 변수가 프로그램에 여전히 있다는 것입니다. 포인터가 포인터가 됩니다. 포인터가 가리키는 메모리가 회수되었기 때문에 이 메모리가 다시 사용되었는지 아니면 여전히 원래 데이터를 저장하고 있는지 알 수 없습니다. 그 후 실수로 이 포인터를 통해 이 메모리 조각의 데이터를 사용하면 읽기나 쓰기에 상관없이 온갖 이상한 문제가 발생하고 우리가 찾기가 어렵습니다. 이 기사에서는 이러한 와일드 포인터 문제의 원인과 위치 지정 방법에 대해 주로 이야기합니다.

2. 와일드 포인터는 어떤 문제를 일으킬 수 있습니까?

개발 중 발생하는 대부분의 EXC_BAD_ACCESS 문제는 와일드 포인터로 인해 발생하며, SIGSEGV 및 SIGBUS의 두 가지 주요 신호가 있습니다. 이 중 SIGSEGV는 작업의 주소가 잘못되었거나 할당되지 않은 메모리에 액세스하거나 쓰기 권한이 없는 메모리에 기록되었음을 나타냅니다. SIGBUS는 잘못된 메모리 유형 액세스를 나타냅니다.

와일드 포인터로 인한 문제는 모든 종류의 이상하고 찾기 어렵습니다. 프로그램에서 와일드 포인터를 사용하는 경우 두 가지 시나리오가 있을 수 있습니다.

1> 액세스한 메모리를 덮어쓰지 않습니다.

원래 객체가 의존하는 다른 객체가 삭제되지 않으면 프로그램의 동작에 문제가 있는 것처럼 보일 수 있지만 실제로는 매우 위험하며 프로그램 논리의 성능이 제어할 수 없습니다.

원본 개체가 의존하는 다른 개체가 삭제되면 내부에 다른 와일드 포인터가 생성될 수 있으며 다양한 복잡한 비정상 시나리오가 계속 발생합니다.

2> 액세스한 메모리를 다시 덮어씁니다.

이 슬레이브 시나리오는 더 번거로울 것이며, 현재 메모리 영역의 접근성이 변경되면 objc_msgSend 실패, SIGBUS 주소 유형 예외, SIGFPE 연산자 예외, SIGILL 명령 예외 등과 같은 많은 유형의 예외가 생성됩니다.

현재 메모리에 액세스할 수 있는 경우 다른 위치에 잘못된 메모리를 작성하여 다른 위치에서 사용할 때 예외가 발생할 수 있습니다. 사용할 데이터 유형이 원래 개체와 일치하지 않아 구현되지 않은 선택기 클래스의 오류, 메서드 클래스 찾기의 오류, 다양한 기본 논리 오류 및 malloc 오류가 발생할 수도 있습니다. 이 시점에서 문제를 해결하는 것은 매우 어렵습니다.

요약하자면, 와일드 포인터의 피해는 매우 큽니다. 비정상적인 충돌을 일으킬 뿐만 아니라 일반적으로 사용되는 다른 코드에 대한 예외도 발생할 수 있으며 재현성 및 임의성이 있습니다. 예를 들어, 특정 Crash 스택을 찾을 수 있습니다. 객체의 메소드가 호출된다는 것. 찾기가 쉽지는 않지만 코드를 검색하면 유사한 메소드 호출을 찾을 수 없습니다. 사실 다른 곳에 와일드 포인터 문제가 있으며 올바른 객체는 그냥 와일드 포인터에 할당된 메모리가 가리키는 와일드 포인터는 이 메모리의 데이터를 파괴합니다. 이 충돌 문제에 대해 우리는 거의 무력합니다.

3. 와일드 포인터 장면을 만들어보십시오.

이전 소개를 통해 우리는 와일드 포인터 문제의 원인과 위험을 이해했습니다. 이제 사용해 볼 수 있습니다. Xcode를 사용하여 새 iOS 프로젝트를 만듭니다. MyObject라는 새 클래스를 만들고 다음과 같이 속성을 추가합니다.

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property(copy) NSString *name;

@end

ViewController 클래스에 다음 테스트 코드를 작성합니다.

#import "ViewController.h"
#import "MyObject.h"
@interface ViewController ()

@property (nonatomic, unsafe_unretained)MyObject *object;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    MyObject *object = [[MyObject alloc] init];
    self.object = object;
    self.object.name = @"HelloWorld";
    void *p = (__bridge void *)(self.object);
    NSLog(@"%p,%@",self.object,self.object.name);
    NSLog(@"%p,%@",p, [(__bridge MyObject *)p name]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%p",self->_object);
    NSLog(@"%@",self.object.name);
}

@end

여기서 우리는 와일드 포인터 문제가 발생하는 장면을 수동으로 생성합니다. ViewController 클래스의 개체 속성은 unsafe_unretained로 선언되었습니다. 이 수정자는 현재 속성이 ARC에 의해 관리되지 않는다는 것을 의미합니다. 참조하는 개체가 해제된 후 이 포인터도 공백으로 남을 것입니다. 위의 코드에서는 viewDidLoad 메소드에 MyObject 객체를 생성하여 현재 컨트롤러의 객체 속성에 복사하고 있으며, 스택에 있는 객체의 라이프 사이클은 현재 코드 블록에서 유효하므로 viewDidLoad 메소드가 종료되면, 이 메모리는 재활용이 될 것이며, 이때 객체 포인터는 와일드 포인터가 됩니다.

다음과 같이 현재 MyOject 개체의 메모리 할당 주소를 관찰하기 위해 viewDidLoad 메서드의 끝에 중단점을 설정할 수 있습니다.

다음 실행에서 개체 개체가 할당한 메모리 주소는 0x600001e542d0(실행마다 다름)이며 나중에 개체의 속성에 액세스하는 것이 실제로 이 메모리의 데이터에 액세스하는 것을 알 수 있습니다. 우리는 메모리 주소를 알고, 우리는 또한 할 수 있습니다 접근을 위한 주소의 직접 사용은 반드시 변수를 필요로 하지 않습니다.예를 들어 위 그림에서 LLDB의 po 명령은 메모리 주소로 직접 메시지를 보낼 수 있습니다.효과는 호출하는 것과 동일합니다. 변수를 통한 객체 메소드

그 후 실행 후 현재 페이지를 클릭할 수 있습니다.대부분의 경우 주소 예외 Crash가 발생합니다.다음과 같이 스레드의 스택 정보를 LLDB를 통해 출력할 수 있습니다.

때로는 프로그램이 기본 메서드에 직접 충돌하여 다음과 같이 더 이상한 스택 정보를 입력할 수 있습니다.

위 그림에서 볼 수 있듯이 스택 정보는 배열의 name 메서드를 호출하라는 메시지를 표시합니다. 이는 실제로 이 메모리 블록이 재할당되었기 때문입니다.

아무런 논리도 없이 데모 프로젝트만 만들었고, 와일드 포인터의 문제는 워낙 다양해서 실제 프로젝트에 있을 경우 와일드 포인터에 문제가 있으면 문제의 원인을 찾기가 더 어렵습니다. 그리고 ARC 환경에서 위의 예제 시나리오는 실제로 확인하기 매우 쉽고, 와일드 포인터의 더 많은 이유는 다중 스레드의 안전하지 않은 데이터 읽기 및 쓰기로 인해 발생합니다. 다중 스레드 사용과 결합하여 야생 포인터의 문제는 더 어렵습니다. 확인하다.

둘째, 원칙에서 와일드 포인터 모니터링

와일드 포인터로 인한 문제를 해결하려면 프로그래밍 외에도 위험한 작성 방법을 피하도록 주의하십시오. 더 중요한 것은 프로세스에서 이러한 문제를 모니터링하기 위해 일련의 솔루션을 요약할 수 있다는 것입니다. 와일드 포인터 문제의 특성상 메모리가 해제될 때 와일드 포인터 문제가 발생할지 실제로 알 수 없으며, 와일드 포인터 문제가 발생한 후 역추적할 수 없습니다. 따라서 이러한 유형의 문제에 대한 모니터링 솔루션을 찾으려면 미리 설정된 아이디어를 사용해야 합니다. 즉, 현재 메모리가 해제된 후에도 액세스할 수 있는 와일드 포인터가 여전히 있다고 가정하면 설계상 이 부분을 실제로 해제할 수 없습니다. 이 메모리 조각을 해제합니다.메모리는 문제가 있는 것으로 표시되며 문제가 있는 메모리에 대한 액세스가 발견되면 와일드 포인터 문제를 나타냅니다. 메모리를 표시할 때 클래스 이름과 같은 원본 개체의 일부 정보도 기록할 수 있으므로 와일드 포인터 문제가 발생하면 특정 Crash 스택이 무엇이든 관계없이 어떤 클래스의 개체 릴리스 문제가 발생하는지 알 수 있습니다. 와일드 포인터는 문제 해결 범위를 크게 좁힐 수 있습니다.

따라서 와일드 포인터 문제를 다루는 핵심 포인트는 다음 두 가지에 있습니다.

1. 표시된 메모리를 사전 설정하고 와일드 포인터 문제가 트리거될 때까지 수동적으로 기다립니다.

2. 와일드 포인터 문제를 일으키는 클래스를 기록하고, 크래시 발생 시 스택 대신 클래스 객체를 사용하여 조사를 시작한다.

위의 두 가지 점에 대해 달성 방법을 살펴 보겠습니다.

1. 좀비 오브젝트

해제될 객체의 메모리는 실제로 재활용되지 않고 표시만 되며 이때 객체를 "좀비 객체"로 시각화합니다. Xcode는 기본적으로 좀비 개체 열기를 지원합니다.좀비 개체에 액세스하면 필연적으로 충돌이 발생하고 콘솔은 관련 프롬프트 정보를 출력합니다. Xcode에서 실행하려는 구성표를 편집하고 다음과 같이 좀비 개체 기능을 켭니다.

프로젝트를 다시 실행하면 프로그램이 충돌한 후 다음 정보가 출력됩니다.

*** -[MyObject retain]: message sent to deallocated instance 0x600000670670

MyObject 개체의 메모리 문제로 인해 와일드 포인터가 충돌했음을 분명히 알 수 있습니다.

Xcode의 좀비 개체 기능은 사용하기 쉽지만 디버깅 중에만 사용할 수 있습니다. 더 자주 우리가 생성하는 와일드 포인터 문제는 온라인 환경에서 발생하고 재현할 수 없습니다. 이 기능은 매우 맛이 없습니다. Xcode에 의존하지 않고 와일드 포인터 모니터링을 구현할 수 있습니까? 먼저 Xcode에서 좀비 객체의 구현 원리를 이해해야 합니다.

2. 애플 좀비 객체의 구현 원리 탐색

우선, 우리는 좀비 객체의 높은 확률을 실현하기 위해 우리는 dealloc 메소드에 무언가를 해야 한다는 것을 대략 알 수 있습니다. 우리는 이 메소드에서 시작하여 단서를 찾고 objc의 소스 코드를 볼 수 있습니다. NSObject.m에서 , 다음 코드를 볼 수 있습니다.

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

주석에서 알 수 있듯이 시스템에서 구현한 좀비 객체는 dealloc 메소드를 다루며, 실제로 NSObject의 dealloc 메소드는 Runtime으로 대체된 것으로 추측된다. CoreFoundation의 소스 코드에는 Zombies에 대한 내용도 있습니다.CFRuntime.c에서 다음 코드를 볼 수 있습니다.

extern void __CFZombifyNSObject(void);  // from NSObject.m

void _CFEnableZombies(void) {
}

그 중 _CFEnableZombies는 이해하기 쉬우며, 좀비 개체 기능이 활성화되어 있는지 여부를 나타내는 데 사용해야 합니다. Xcode에서 설정한 환경 변수 기능과 일치해야 합니다. __CFZombifyNSObject는 주석에서 알 수 있습니다. 구현해야 합니다. 좀비 개체의. Xcode에 __CFZombifyNSObject의 기호 중단점을 추가하고 중단점 이후의 내용은 다음과 같습니다.

여기에서 어셈블리를 보면 너무 낯설지 않을 것입니다.우리는 대략 다음과 같은 핵심 의사 코드를 제안합니다.

// 定义字符串
define "NSObject"
// 用来获取NSObject类
objc_lookUpClass "NSObject"
// 定义字符串
define "dealloc"
define "__dealloc_zombie"
// 获取dealloc方法的实现
class_getInstanceMethod "NSObject" "dealloc"
// 获取__dealloc_zombie方法的实现
class_getInstanceMethod "NSObject" "__dealloc_zombie"
// 交换dealloc与__dealloc_zombie的方法实现
method_exchangeImplementations "dealloc" "__dealloc_zombie"

생각한 것과 유사하게 __dealloc_zombie의 기호 중단점을 추가하여 다음과 같이 __dealloc_zombie 메서드가 구현되는 방식을 확인할 수 있습니다.

CoreFoundation`-[NSObject(NSObject) __dealloc_zombie]:
->  0x10ef77c49 <+0>:   pushq  %rbp
    0x10ef77c4a <+1>:   movq   %rsp, %rbp
    0x10ef77c4d <+4>:   pushq  %r14
    0x10ef77c4f <+6>:   pushq  %rbx
    0x10ef77c50 <+7>:   subq   $0x10, %rsp
    0x10ef77c54 <+11>:  movq   0x2e04fd(%rip), %rax      ; (void *)0x0000000110021970: __stack_chk_guard
    0x10ef77c5b <+18>:  movq   (%rax), %rax
    0x10ef77c5e <+21>:  movq   %rax, -0x18(%rbp)
    0x10ef77c62 <+25>:  testq  %rdi, %rdi
    0x10ef77c65 <+28>:  js     0x10ef77d04               ; <+187>
    0x10ef77c6b <+34>:  movq   %rdi, %rbx
    0x10ef77c6e <+37>:  cmpb   $0x0, 0x488703(%rip)      ; __CFConstantStringClassReferencePtr + 7
    0x10ef77c75 <+44>:  je     0x10ef77d1d               ; <+212>
    0x10ef77c7b <+50>:  movq   %rbx, %rdi
    0x10ef77c7e <+53>:  callq  0x10eff4b52               ; symbol stub for: object_getClass
    0x10ef77c83 <+58>:  leaq   -0x20(%rbp), %r14
    0x10ef77c87 <+62>:  movq   $0x0, (%r14)
    0x10ef77c8e <+69>:  movq   %rax, %rdi
    0x10ef77c91 <+72>:  callq  0x10eff464e               ; symbol stub for: class_getName
    0x10ef77c96 <+77>:  leaq   0x242db5(%rip), %rsi      ; "_NSZombie_%s"
    0x10ef77c9d <+84>:  movq   %r14, %rdi
    0x10ef77ca0 <+87>:  movq   %rax, %rdx
    0x10ef77ca3 <+90>:  xorl   %eax, %eax
    0x10ef77ca5 <+92>:  callq  0x10eff4570               ; symbol stub for: asprintf
    0x10ef77caa <+97>:  movq   (%r14), %rdi
    0x10ef77cad <+100>: callq  0x10eff4ab0               ; symbol stub for: objc_lookUpClass
    0x10ef77cb2 <+105>: movq   %rax, %r14
    0x10ef77cb5 <+108>: testq  %rax, %rax
    0x10ef77cb8 <+111>: jne    0x10ef77cd7               ; <+142>
    0x10ef77cba <+113>: leaq   0x2427aa(%rip), %rdi      ; "_NSZombie_"
    0x10ef77cc1 <+120>: callq  0x10eff4ab0               ; symbol stub for: objc_lookUpClass
    0x10ef77cc6 <+125>: movq   -0x20(%rbp), %rsi
    0x10ef77cca <+129>: movq   %rax, %rdi
    0x10ef77ccd <+132>: xorl   %edx, %edx
    0x10ef77ccf <+134>: callq  0x10eff4a62               ; symbol stub for: objc_duplicateClass
    0x10ef77cd4 <+139>: movq   %rax, %r14
    0x10ef77cd7 <+142>: movq   -0x20(%rbp), %rdi
    0x10ef77cdb <+146>: callq  0x10eff482e               ; symbol stub for: free
    0x10ef77ce0 <+151>: movq   %rbx, %rdi
    0x10ef77ce3 <+154>: callq  0x10eff4a5c               ; symbol stub for: objc_destructInstance
    0x10ef77ce8 <+159>: movq   %rbx, %rdi
    0x10ef77ceb <+162>: movq   %r14, %rsi
    0x10ef77cee <+165>: callq  0x10eff4b6a               ; symbol stub for: object_setClass
    0x10ef77cf3 <+170>: cmpb   $0x0, 0x48867f(%rip)      ; __CFZombieEnabled
    0x10ef77cfa <+177>: je     0x10ef77d04               ; <+187>
    0x10ef77cfc <+179>: movq   %rbx, %rdi
    0x10ef77cff <+182>: callq  0x10eff482e               ; symbol stub for: free
    0x10ef77d04 <+187>: movq   0x2e044d(%rip), %rax      ; (void *)0x0000000110021970: __stack_chk_guard
    0x10ef77d0b <+194>: movq   (%rax), %rax
    0x10ef77d0e <+197>: cmpq   -0x18(%rbp), %rax
    0x10ef77d12 <+201>: jne    0x10ef77d3d               ; <+244>
    0x10ef77d14 <+203>: addq   $0x10, %rsp
    0x10ef77d18 <+207>: popq   %rbx
    0x10ef77d19 <+208>: popq   %r14
    0x10ef77d1b <+210>: popq   %rbp
    0x10ef77d1c <+211>: retq   
    0x10ef77d1d <+212>: movq   0x2e0434(%rip), %rax      ; (void *)0x0000000110021970: __stack_chk_guard
    0x10ef77d24 <+219>: movq   (%rax), %rax
    0x10ef77d27 <+222>: cmpq   -0x18(%rbp), %rax
    0x10ef77d2b <+226>: jne    0x10ef77d3d               ; <+244>
    0x10ef77d2d <+228>: movq   %rbx, %rdi
    0x10ef77d30 <+231>: addq   $0x10, %rsp
    0x10ef77d34 <+235>: popq   %rbx
    0x10ef77d35 <+236>: popq   %r14
    0x10ef77d37 <+238>: popq   %rbp
    0x10ef77d38 <+239>: jmp    0x10eff44c8               ; symbol stub for: _objc_rootDealloc
    0x10ef77d3d <+244>: callq  0x10eff443e               ; symbol stub for: __stack_chk_fail

어셈블리 내용이 많고 전체적인 과정이 비교적 명확하다.pseudocode는 다음과 같다.

// 获取当前类
object_getClass
// 通过当前类获取当前类型
class_getName
// 将_NSZombie_拼接上当前类名
zombiesClsName = "_NSZombie_%s" + className
// 获取zombiesClsName类
objc_lookUpClass zombiesClsName
// 判断是否已经存在zombiesCls
if not zombiesCls:
    // 如果不存在 
    // 现获取"_NSZombie_"类
    cls = objc_lookUpClass "_NSZombie_"
    // 复制出一个cls类,类名为zombiesClsName
    objc_duplicateClass cls zombiesClsName
// 字符串变量释放
free zombiesClsName
// objc中原本的对象销毁方法
objc_destructInstance(self)
// 将当前对象的类修改为zombiesCls
object_setClass zombiesCls
// 判断是否开启了僵尸对象功能
if not __CFZombieEnabled:
    // 如果没开启 将当前内存释放掉
    free

위의 의사 코드는 기본적으로 __dealloc_zombie 메서드 구현의 전체 과정이며, objc 소스 코드에서 NSObject 클래스의 원래 dealloc 메서드 구현 경로는 다음과 같습니다.

- (void)dealloc {
    _objc_rootDealloc(self);
}

void _objc_rootDealloc(id obj)
{
    ASSERT(obj);
    obj->rootDealloc();
}

inline void objc_object::rootDealloc()
{
    // taggedPointer无需回收内存
    if (isTaggedPointer()) return;  // fixme necessary?
    // nonpointer为1表示不只是地址,isa中包含了其他信息
    // weakly_referenced表示是否有弱引用
    // has_assoc 表示是否有关联属性
    // has_cxx_dtor 是否需要C++或Objc析构
    // has_sidetable_rc是否有散列表计数引脚
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    { 
        // 如果都没有 直接回收内存
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
id object_dispose(id obj)
{
    if (!obj) return nil;
    // 进行内存回收前的销毁工作
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

__dealloc_zombie와 real dealloc의 구현은 실제로 현재 메모리의 복구 부분일 뿐이고 objc_destructInstance 메소드가 정상적으로 실행되는 것을 알 수 있다 이 메소드는 다음과 같이 구현된다:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();
        // C++ 析构
        if (cxx) object_cxxDestruct(obj);
        // 移除关联属性
        if (assoc) _object_remove_assocations(obj);
        // 弱引用表和散列表的清除
        obj->clearDeallocating();
    }

    return obj;
}

위의 분석을 통해 우리는 시스템에서 구현한 좀비 객체가 실제로 매우 안전하고 정상적인 코드의 동작에 부정적인 영향을 미치지 않는다는 것을 발견했습니다. 메모리 사용량은 있지만 특정 전략을 통해 해제할 수 있습니다. .

3. 온라인 와일드 포인터 문제 수집 수동 구현

디버그 환경에 의존하지 않고 시스템 좀비 개체의 구현 원리를 이해한 후에는 이 아이디어에 따라 좀비 개체 모니터링 기능을 구현할 수도 있습니다.

1. Apple의 좀비 개체 아이디어를 모델로 한 것

먼저 다음과 같이 구현된 _YHZombie_라는 템플릿 클래스를 만듭니다.

// _YHZombie_.h
#import <Foundation/Foundation.h>

@interface _YHZombie_ : NSObject

@end


//  _YHZombie_.m
#import "_YHZombie_.h"

@implementation _YHZombie_

// 调用这个对象对的所有方法都hook住进行LOG
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%p-[%@ %@]:%@",self ,[NSStringFromClass(self.class) componentsSeparatedByString:@"_YHZombie_"].lastObject, NSStringFromSelector(aSelector), @"向已经dealloc的对象发送了消息");
    // 结束当前线程
    abort();
}

@end

다음과 같이 dealloc 메서드를 대체할 새 NSObject 범주를 만듭니다.

//  NSObject+YHZombiesNSObject.h
#import <Foundation/Foundation.h>

@interface NSObject (YHZombiesNSObject)

@end


//  NSObject+YHZombiesNSObject.m
#import "NSObject+YHZombiesNSObject.h"
#import <objc/objc.h>
#import <objc/runtime.h>

@implementation NSObject (YHZombiesNSObject)

+(void)load {
    [self __YHZobiesObject];
}

+ (void)__YHZobiesObject {
    char *clsChars = "NSObject";
    Class cls = objc_lookUpClass(clsChars);
    Method oriMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"dealloc"));
    Method newMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"__YHDealloc_zombie"));
    method_exchangeImplementations(oriMethod, newMethod);
    
}

- (void)__YHDealloc_zombie {
    const char *className = object_getClassName(self);
    char *zombieClassName = NULL;
    asprintf(&zombieClassName, "_YHZombie_%s", className);
    Class zombieClass = objc_getClass(zombieClassName);
    if (zombieClass == Nil) {
        zombieClass = objc_duplicateClass(objc_getClass("_YHZombie_"), zombieClassName, 0);
    }
    objc_destructInstance(self);
    object_setClass(self, zombieClass);
    if (zombieClassName != NULL)
    {
        free(zombieClassName);
    }
}


@end

위의 코드는 일부 내결함성 판단을 제외하고 시스템의 좀비 개체와 동일한 아이디어를 가지고 있습니다.

테스트 코드를 다시 실행하십시오. 와일드 포인터에 액세스할 때 예외가 100% 생성되고 출력은 다음과 같습니다.

0x600003a8c2e0-[MyObject name]:向已经dealloc的对象发送了消息

이제 원칙적으로 Xcode에 의존하지 않는 와일드 포인터 모니터링 도구를 간단히 구현했습니다.

2. 모니터링을 C 포인터로 확장

개체의 좀비화를 통해 OC 계층의 와일드 포인터 문제를 잘 모니터링할 수 있지만 이 방법은 C 계층의 포인터에는 실용적이지 않습니다. 관리 방법 참조 카운팅이 없으면 Hook dealloc을 사용하여 개체를 좀비화할 수 없습니다. 예를 들어 다음과 같은 구조체를 만듭니다.

typedef struct {
    NSString *name;
} MyStruct;

이 구조를 사용할 때 초기화 전이나 메모리 회수 후에 사용하면 다음과 같이 와일드 포인터 문제가 발생할 수 있습니다.

MyStruct *p;
p = malloc(sizeof(MyStruct));
// 此时内存中的数据不可控 可能是之前未擦除的
printf("%x\n", *((int *)p));
// 使用可能会出现野指针问题
NSLog(@"%@", p->name);
// 进行内存数据的初始化
p->name = @"HelloWorld";
// 回收内存
free(p);
// 此时内存中的数据不可控
NSLog(@"%@", p->name);

위의 와일드 포인터 장면에 대한 주요 이유를 생각할 수 있습니다.

1. 할당된 메모리를 획득한 후 이전에 메모리를 사용한 적이 있는 경우 현재 데이터를 제어할 수 없으며 현재 포인터가 이 데이터를 직접 사용하는 경우 문제가 발생합니다.

2. 메모리가 회수된 후 현재 메모리의 데이터는 제어할 수 없으며 이전에 지워지지 않은 다른 포인터나 포인터가 사용할 수 있습니다.

위의 시나리오와 상관없이 이 와일드 포인터 문제는 매우 무작위적이며 디버그하기 어렵습니다. 따라서 우리가 핵심에서 처리해야 하는 것은 임의성을 불가피성으로 변경하는 것입니다. 즉, 이러한 문제가 있는 메모리를 사용할 때 충돌 가능성이 있는 대신 직접 충돌하는 방법을 찾는 것입니다. 시나리오 1을 처리하는 것은 쉽습니다. C에서 malloc 메소드를 연결하고 메모리를 할당한 후 미리 정해진 예외 데이터를 메모리에 직접 쓸 수 있으므로 초기화 전에 이 데이터를 사용하면 필연적으로 충돌이 발생합니다. 시나리오 2의 경우 C에서 free 메소드를 연결하고 메모리를 회수한 후 합의된 예외 데이터를 이 메모리에 직접 쓸 수 있습니다.다음에 메모리를 다시 할당하지 않으면 사용 후 불가피하게 충돌이 발생합니다. Xcode에서 제공하는 Malloc Scribble 디버깅 기능은 이렇게 구현되어 있습니다.

Xcode의 Malloc Scribble 옵션을 켜고 위의 코드를 실행하면 아래와 같은 효과가 나타납니다.

malloc이 메모리를 할당한 후 모든 바이트가 0xAA로 채워지고 초기화 전에 사용하면 필연적으로 Crash가 발생함을 알 수 있습니다. 이는 Apple 공식 문서의 설명과 일치하지만, 해제된 후 얻은 메모리 데이터는 문서에 나와 있는 대로 0x55가 아닐 수 있습니다. 이 메모리는 다른 콘텐츠로 덮어쓸 수 있기 때문입니다. 공식 웹 사이트 문서는 다음과 같이 설명됩니다.

Malloc Scribble의 아이디어에 따라 와일드 포인터 문제를 무작위에서 불가피하게 변경하는 도구를 수동으로 구현할 수도 있습니다. 시스템 malloc 관련 기능과 자유 기능을 다시 작성하기만 하면 됩니다. C 언어 기능의 Hook의 경우 fishhook 라이브러리를 직접 사용할 수 있습니다.

https://github.com/facebook/fishhook

위의 라이브러리를 가져온 후 다음과 같이 구현되는 YHMallocScrbble이라는 새 클래스를 만듭니다.

//  YHMallcScrbble.h
#import <Foundation/Foundation.h>

@interface YHMallcScrbble : NSObject

@end

//  YHMallcScrbble.m
#import "YHMallcScrbble.h"
#import "fishhook.h"
#import "malloc/malloc.h"


void * (*orig_malloc)(size_t __size);
void (*orig_free)(void * p);


void *_YHMalloc_(size_t __size) {
    void *p = orig_malloc(__size);
    memset(p, 0xAA, __size);
    return p;
}

void _YHFree_(void * p) {
    size_t size = malloc_size(p);
    memset(p, 0x55, size);
    orig_free(p);
}



@implementation YHMallcScrbble

+ (void)load {
    rebind_symbols((struct rebinding[2]){{"malloc", _YHMalloc_, (void *)&orig_malloc}, {"free", _YHFree_, (void *)&orig_free}}, 2);
}

@end

이런 식으로 우리는 와일드 포인터 문제가 무작위에서 불가피하게 바뀌고 C 포인터가 보편적이라는 것을 깨닫습니다.

Malloc Scribble 방식은 좀비 객체 방식과 비교하여 보편적으로 C 포인터를 사용할 수 있으며 일시적으로 메모리를 사용하지 않고 객체 메모리 복구를 진정으로 실현합니다. 예를 들어, free 이후에 쓰여진 0x55는 많은 경우에 유효하지 않습니다. 물론 커스텀 free 메소드에서도 원본 시스템의 free 메소드를 호출할 수 없으므로 강제로 메모리를 할당할 수 없으며 이는 실제로 좀비 객체 방식과 유사합니다. 그리고 좀비 오브젝트 스킴에 비해 Malloc Scribble은 임의성을 어느 정도만 변경할 수 있어 문제 노출에 편리하지만, 개발자 입장에서는 어떤 유형의 데이터가 문제인지 알려주는 정보가 많지 않습니다. .

넷째, 일부 확장

위의 내용은 와일드 포인터 문제를 모니터링하는 몇 가지 방법과 원칙에 대한 간략한 소개입니다. 좀비 개체와 Malloc Scribble 외에도 Xcode는 메모리 문제를 모니터링하는 Address Sanitizer 도구도 제공합니다.또한 원칙은 malloc 및 free 함수를 처리하는 것이지만 프로그램이 문제가 있는 메모리에 액세스하면 시간이 지나면 충돌할 수 있습니다. 동시에 이 도구는 malloc 동안 객체의 스택 정보를 저장할 수 있으므로 문제를 찾을 수 있습니다. 어떤 방법을 택하든 정말로 온라인에서 실행하고 싶다면 데이터 수집 전략, 좀비 객체 메모리의 정리 타이밍, 문제를 판단하고 스택을 잡아야 하는 시점 등과 같이 여전히 해야 할 일이 많습니다. .

마지막으로, 이 기사가 개발 과정에서 와일드 포인터 문제를 처리하기 위한 몇 가지 아이디어를 제공할 수 있기를 바랍니다. 이 기사에서 작성된 샘플 코드는 다음 주소에서 다운로드할 수 있습니다.

https://github.com/ZYHshao/ZombiesDemo

기술에 집중하고, 사랑을 이해하고, 기꺼이 공유하고, 친구가 됩니다.

QQ:316045346

{{o.name}}
{{m.name}}

추천

출처my.oschina.net/u/2340880/blog/5341605