
中文
디버깅은 평판이 좋지 않습니다. 내 말은, 개발자가 프로그램을 완전히 이해했다면 버그가 없을 것이며 처음에는 디버깅하지 않을 것입니다.
그렇게 생각하지 마십시오.
소프트웨어 또는 그 문제에 대한 소프트웨어에는 항상 버그가있을 것입니다. 제품 관리자가 부과 한 많은 양의 테스트 범위가이를 해결하지 못할 것입니다. 실제로, 디버깅을 단지 깨지는 것을 고치는 과정으로 보는 것은 실제로 당신의 분석 능력을 정신적으로 방해 할 수있는 유독 한 사고 방식입니다.
대신 디버깅을 단순히 프로그램을 더 잘 이해하기위한 프로세스로 보아야합니다. 그것은 미묘한 차이이지만, 당신이 진정으로 그것을 믿는다면, 디버깅의 이전의 드루게리는 단순히 사라집니다.
Cobol Language의 창립자 인 Grace Hopper는 릴레이 컴퓨터에서 세계 최초의 버그를 발견 한 이후 소프트웨어 개발의 버그 생성은 결코 멈추지 않았습니다. Advanced Apple Debugging & Reverse Engineering of Book의 서문에 따르면, 개발자는 소프트웨어의 작동 방식에 대한 이해가 좋으면 버그가 없을 것이라고 생각하지 않습니다. 따라서 디버깅은 소프트웨어 개발 수명주기에서 거의 피할 수없는 단계입니다.
경험이없는 프로그래머에게 디버깅을 정의하는 방법에 대해 물어 보면 "디버깅은 소프트웨어 문제에 대한 솔루션을 찾기 위해하는 일"이라고 말할 수 있습니다. 그는 옳지 만 그것은 실제 디버깅의 작은 부분 일뿐입니다.
실제 디버깅 단계는 다음과 같습니다.
위의 단계 중에서 가장 중요한 단계는 첫 번째 단계입니다. 문제를 찾으십시오. 분명히, 그것은 다른 단계의 전제 조건입니다.
연구에 따르면 경험이 풍부한 프로그래머가 동일한 결함 세트를 찾기 위해 디버깅에 소비되는 시간은 미숙 한 프로그래머의 약 20 명입니다. 즉, 디버깅 경험은 프로그래밍 효율성에 엄청난 차이를 만듭니다. 우리는 소프트웨어 디자인에 관한 많은 책을 가지고 있습니다. 불행히도, 드문 경우에는 디버깅에 대한 소개, 심지어 학교 과정까지도 소개됩니다.
수년에 걸쳐 디버거가 개선됨에 따라 프로그래머의 코딩 스타일이 철저히 변경됩니다. 물론, 디버거는 좋은 사고를 대체 할 수 없으며, 사고는 훌륭한 디버거를 대체 할 수 없으며, 가장 완벽한 조합은 훌륭한 사고와 훌륭한 디버거입니다.
다음 그래프는 Book <디버깅 : 가장 애매한 소프트웨어 및 하드웨어 문제를 찾기위한 9 가지 필수 규칙>에 설명 된 9 개의 디버깅 규칙입니다.

iOS 프로그래머로서 대부분의 시간은 어셈블리 언어를 다루지 않지만 특히 소스 코드가없는 시스템 프레임 워크 또는 타사 프레임 워크를 디버깅 할 때 어셈블리가 여전히 도움이된다는 것을 이해합니다.
Asssembly Language는 낮은 수준의 기계 지향 프로그래밍 언어로 다양한 CPU를위한 기계 지침을위한 니모닉 모음으로 생각할 수 있습니다. 프로그래머는 어셈블리 언어를 사용하여 컴퓨터 하드웨어 시스템을 직접 제어 할 수 있습니다. 그리고 어셈블리 언어로 작성된 프로그램은 빠른 실행 속도와 메모리가 점유되지 않은 것과 같은 많은 장점을 가지고 있습니다.
지금까지 Apple 플랫폼, x86 및 ARM에는 두 개의 주요 아키텍처가 널리 사용됩니다. ARM 어셈블리 언어를 사용하는 모바일 장치에서는 주로 ARM이 감소 된 명령 세트 컴퓨팅 (RISC) 아키텍처이기 때문에 저전력 소비 이점이 있습니다. Mac OS와 같은 데스크탑 플랫폼은 X86 아키텍처가 사용됩니다. iOS 시뮬레이터에 설치된 앱은 실제로 시뮬레이터 내에서 Mac OS 앱으로 실행 중이므로 시뮬레이터가 컨테이너처럼 작동합니다. 우리의 사례는 iOS 시뮬레이터에서 디버깅되었으므로 주요 연구 목표는 X86 어셈블리 언어입니다.
X86 어셈블리 언어는 두 개의 구문 분기로 발전합니다 : 인텔 (X86 플랫폼 문서에서 Originally 사용)과 AT & T. Intel은 MS-DOS 및 Windows 제품군을 지배하는 반면 AT & T는 UNIX 제품군에서 일반적입니다. 인텔과 AT & T 간의 구문에는 가변, 상수, 레지스터 액세스, 간접 주소 지정 및 오프셋과 같은 구문에는 큰 차이가 있습니다. 그들의 구문 차이는 거대하지만 하드웨어 시스템은 동일하므로 그중 하나가 다른 하나로 원활하게 마이그레이션 될 수 있습니다. AT & T 어셈블리 언어는 Xcode에서 사용되므로 아래 부분의 AT & T 에 중점을 둘 것입니다.
인텔 구문은 호퍼 분해 및 IDA Pro의 분해 도구에 사용됩니다.
인텔과 AT & T의 차이점은 다음과 같습니다.
Operand : AT & T 구문의 접두사 % Registers 이름의 접두사로 사용되며 $ 즉각적인 오페라의 접두사로 사용되며 인텔의 레지스터 및 즉각적인 오페라에는 접두사가 사용되지 않습니다. 다른 차이점은 0x AT & T에서 16 진수의 접두사로 추가된다는 것입니다. 아래 차트는 접두사의 차이를 보여줍니다.
| AT & T | 인텔 |
|---|---|
| MOVQ %RAX, %RBX | MOV RBX, RAX |
| addq $ 0x10, %rsp | RSP, 010H를 추가하십시오 |
Intel Syntax에서
h접미사는 16 진 피연산자에 사용되며b접미사는 이진 피연산자에 사용됩니다.
오페라 : AT & T 구문에서 첫 번째 피연산자는 소스 피연산자이며, 두 번째 피연산자는 대상 피연산자입니다. 그러나 인텔 구문에서는 피연산자의 순서가 반대입니다. 이 시점부터 AT & T의 구문은 독서 습관에 따라 우리에게 더 편안합니다.
주소 지정 모드 : Intel 구문과 비교하여 AT & T의 간접 주소 지정 모드는 읽기 어렵습니다. 그러나 주소 계산의 알고리즘은 address = disp + base + index * scale 입니다. base 기본 주소를 나타내고, disp 는 오프셋 주소를 나타내고, index * scale 요소의 위치를 결정하고, scale 2의 전력 일 수있는 요소의 크기입니다. disp/base/index/scale 은 모두 선택 사항이고 index 의 기본값은 %segreg: disp(base,index,scale) 이고 scale 의 기본값은 1입니다. 이제 주소 계산의 segreg: [base+index*scale+disp] 을 보겠습니다. 실제로, 두 개의 지침은 둘 다 세그먼트 주소 지정 모드에 속합니다. segreg CPU의 숫자 용량이 레지스터 숫자를 넘어서 다루는 실제 모드에서 일반적으로 사용되는 세그먼트 레지스터를 나타냅니다. 예를 들어 CPU는 20 비트 공간을 처리 할 수 있지만 레지스터에는 16 비트 만 있습니다. 20 자리 공간을 달성하려면 다른 주소 지정 모드를 사용해야합니다 : segreg:offset . 이 주소 지정 모드를 사용하면 오프셋 주소가 segreg * 16 + offset 되지만 플랫 메모리 모드보다 더 복잡합니다. 보호 모드에서 주소 지정은 선형 주소 공간 아래에 있으므로 세그먼트 기본 주소를 무시할 수 있습니다.
| AT & T | 인텔 |
|---|---|
| MOVQ 0xB57751 ( %RIP), %RSI | Mov RSI, QWORD PTR [RIP+0XB57751H] |
| LEAQ (%RAX,%RBX, 8),%RDI | Lea Rdi, Qword Ptr [RAX+RBX*8] |
즉각적인 피연산자가
disp또는scale의 장소에 오면$접미사를 생략 할 수 있습니다. Intel Syntax,byte ptr,word ptr,dword ptr및qword ptr에서 메모리 피연산자 전에 추가해야합니다.
Opcode의 접미사 : AT & T 구문에서 모든 Opcode에는 크기를 지정하는 접미사가 있습니다. 일반적으로 4 가지 접미사에는 b , w , l 및 q 있습니다. b 8 비트 바이트를 나타내고 w 16 비트 단어를 의미하며 l 32 비트 이중 단어를 의미합니다. 32 자리 단어는 16 비트의 긴 단어라고도합니다. q 64 비트 쿼드 워드를 나타냅니다. 아래 차트는 AT & T 및 Intel의 데이터 전환 명령어 (MOV)의 구문을 보여줍니다.
| AT & T | 인텔 |
|---|---|
| movb %al, %bl | Mov Bl, al |
| Movw %AX, %BX | Mov BX, AX |
| movl %eax, %ebx | Mov Ebx, Eax |
| MOVQ %RAX, %RBX | MOV RBX, RAX |
아시다시피, 메모리는 CPU에 대한 지침 및 데이터를 저장하는 데 사용됩니다. 메모리는 본질적으로 바이트 배열입니다. 메모리 액세스 속도는 매우 빠르지 만 CPU의 명령 실행 속도를 높이기 위해 더 작고 빠른 저장 장치가 필요합니다. 명령 실행 중에 모든 데이터는 일시적으로 레지스터에 저장됩니다. 그것이 레지스터의 이름이 지정된 이유입니다.
프로세서가 16 비트에서 32 비트에서 32 비트로 증가하면 8 개의 레지스터도 32 비트로 확장됩니다. 그 후, 확장 레지스터를 사용하면 E 접두사가 원래 레지스터 이름에 추가됩니다. 32 비트 프로세서는 Intel Architecture 32 비트이며 IA32입니다. 오늘날 주요 프로세서는 64 비트 인텔 아키텍처로 IA32에서 확장되어 X86-64라고합니다. IA32는 과거이기 때문에이 기사는 x86-64에만 초점을 맞출 것입니다. x86-64에서 레지스터의 양은 8에서 16으로 연장됩니다.이 확장으로 인해 프로그램 상태는 레지스터에 저장 될 수 있지만 스택은 스택에 저장 될 수 있습니다. 따라서 메모리 액세스 빈도가 크게 줄어 듭니다.
X86-64에는 16 개의 64 비트 일반 레지스터와 16 개의 플로팅 포인터 레지스터가 있습니다. 또한 CPU에는 rip 라는 64 비트 명령어 포인터 레지스터가 하나 더 있습니다. 다음 실행 된 명령의 주소를 저장하도록 설계되었습니다. 널리 사용되지 않은 다른 레지스터도 있습니다. 우리는이 기사에서 그것에 대해 이야기하려고하지 않습니다. 16 개의 일반 레지스터 중 8 개는 IA32에서 나온 것입니다. 다른 8 개의 일반 레지스터는 x86-64 이후 R8 -R15 이후 새로 추가되었습니다. 16 개의 플로팅 레지스터는 XMM0 -XMM15입니다.
현재 CPU는 8088에서, 레지스터는 16 비트에서 32 비트로, 마지막으로 64 비트로 연장됩니다. 따라서이 프로그램은 여전히 낮은 8 비트 또는 16 비트 또는 32 비트의 레지스터에 액세스 할 수 있습니다.
아래 차트는 x86-64의 16 개의 일반 레지스터를 보여줍니다.

lldb에서 register read 명령을 사용하면 현재 스택 프레임의 레지스터 데이터를 덤프 할 수 있습니다.
예를 들어, 아래 명령을 사용하여 레지스터의 모든 데이터를 표시 할 수 있습니다.
register read -a or register read --all
General Purpose Registers:
rax = 0x00007ff8b680c8c0
rbx = 0x00007ff8b456fe30
rcx = 0x00007ff8b6804330
rdx = 0x00007ff8b6804330
rdi = 0x00007ff8b456fe30
rsi = 0x000000010cba6309 "initWithTask:delegate:delegateQueue:"
rbp = 0x000070000f1bcc90
rsp = 0x000070000f1bcc18
r8 = 0x00007ff8b680c8c0
r9 = 0x00000000ffff0000
r10 = 0x00e6f00100e6f080
r11 = 0x000000010ca13306 CFNetwork`-[__NSCFURLLocalSessionConnection initWithTask:delegate:delegateQueue:]
r12 = 0x00007ff8b4687c70
r13 = 0x000000010a051800 libobjc.A.dylib`objc_msgSend
r14 = 0x00007ff8b4433bd0
r15 = 0x00007ff8b6804330
rip = 0x000000010ca13306 CFNetwork`-[__NSCFURLLocalSessionConnection initWithTask:delegate:delegateQueue:]
rflags = 0x0000000000000246
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
eax = 0xb680c8c0
ebx = 0xb456fe30
ecx = 0xb6804330
edx = 0xb6804330
edi = 0xb456fe30
esi = 0x0cba6309
ebp = 0x0f1bcc90
esp = 0x0f1bcc18
r8d = 0xb680c8c0
r9d = 0xffff0000
r10d = 0x00e6f080
r11d = 0x0ca13306
r12d = 0xb4687c70
r13d = 0x0a051800
r14d = 0xb4433bd0
r15d = 0xb6804330
ax = 0xc8c0
bx = 0xfe30
cx = 0x4330
dx = 0x4330
di = 0xfe30
si = 0x6309
bp = 0xcc90
sp = 0xcc18
r8w = 0xc8c0
r9w = 0x0000
r10w = 0xf080
r11w = 0x3306
r12w = 0x7c70
r13w = 0x1800
r14w = 0x3bd0
r15w = 0x4330
ah = 0xc8
bh = 0xfe
ch = 0x43
dh = 0x43
al = 0xc0
bl = 0x30
cl = 0x30
dl = 0x30
dil = 0x30
sil = 0x09
bpl = 0x90
spl = 0x18
r8l = 0xc0
r9l = 0x00
r10l = 0x80
r11l = 0x06
r12l = 0x70
r13l = 0x00
r14l = 0xd0
r15l = 0x30
Floating Point Registers:
fctrl = 0x037f
fstat = 0x0000
ftag = 0x00
fop = 0x0000
fioff = 0x00000000
fiseg = 0x0000
fooff = 0x00000000
foseg = 0x0000
mxcsr = 0x00001fa1
mxcsrmask = 0x0000ffff
stmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xff 0xff}
stmm1 = {0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0xff 0xff}
stmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
stmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
stmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0xbc 0x87 0x0b 0xc0}
stmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
stmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x78 0xbb 0x0b 0x40}
stmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
Exception State Registers:
trapno = 0x00000003
err = 0x00000000
faultvaddr = 0x000000010bb91000
우리가 알다시피, x86-64 : xmm0 -xmm15에는 16 개의 부동 포인터 레지스터가 있습니다. 사실, 다른 세부 사항이 있습니다. register read -a 명령의 출력에서 XMM 레지스터 그룹 외에 STMM 및 YMM 레지스터가 있음을 알 수 있습니다. 여기서 STMM은 ST 레지스터의 별칭이며 ST는 플로트 데이터를 처리하기 위해 X86의 FPU (플로트 포인트 유닛) 레지스터입니다. FPU에는 80 비트 플로트 포인터 레지스터가있는 1 개의 플로트 포인터 레지스터가 포함되어 있습니다 : ST0 -ST7. STMM 레지스터가 출력에서 80 비트라는 것을 알 수 있습니다. 이는 STMM 레지스터가 ST 레지스터임을 증명할 수 있습니다. XMM은 128 비트 레지스터이며 YMM 레지스터는 256 비트이며 XMM의 확장입니다. 실제로 XMM 레지스터는 128 비트의 YMM 레지스터입니다. EAX 레지스터와 마찬가지로 32 비트의 RAX 레지스터입니다. Pentium III에서 Intel은 MMX의 확장 인 SSE (Streaming Simd Extensions)라는 지침 세트를 게시했습니다. 8 개의 새로운 128 비트 레지스터 (XMM0 -XMM7)가 SSE에 추가됩니다. AVX (Advanced Vector Extensions) 명령어 세트는 SSE의 확장 아키텍처입니다. 또한 AVX에서 128 비트 레지스터 XMM은 256 비트 레지스터 YMM으로 확장되었습니다.

함수 호출에는 한 편집 장치에서 다른 컴파일 장치로 전달 및 제어 전송이 포함됩니다. 기능 호출 절차에서 데이터 전달, 로컬 변수 할당 및 릴리스는 스택에 의해 수행됩니다. 단일 기능 호출에 할당 된 스택을 스택 프레임이라고합니다.
OS X X86-64의 기능 호출 규칙은 기사에 설명 된 규칙과 동일합니다. 시스템 V 응용 이진 인터페이스 AMD64 아키텍처 프로세서 보충. 따라서 관심이 있으시면 참조 할 수 있습니다.
LLDB 디버깅 중에 bt 명령을 사용하여 다음과 같이 현재 스레드의 스택 추적을 인쇄 할 수 있습니다.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001054e09d4 TestDemo`-[ViewController viewDidLoad](self=0x00007fd349558950, _cmd="viewDidLoad") at ViewController.m:18
frame #1: 0x00000001064a6931 UIKit`-[UIViewController loadViewIfRequired] + 1344
frame #2: 0x00000001064a6c7d UIKit`-[UIViewController view] + 27
frame #3: 0x00000001063840c0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
// many other frames are ommitted here
실제로, bt 명령은 스택 프레임에 따라 작동합니다. 스택 프레임은 함수 실행의 컨텍스트로 볼 수있는 함수의 반환 주소 및 로컬 변수를 보존합니다. 우리가 알다시피, 힙은 위쪽으로 자라며 스택은 큰 수준의 메모리 주소에서 작은 수치에 이르기까지 스택이 아래쪽으로 자랍니다. 함수가 호출되면 함수 호출에 하나의 독립형 스택 프레임이 할당됩니다. 프레임 포인터라고 불리는 RBP 레지스터는 항상 최신 할당 된 스택 프레임 (높은 주소)의 끝을 가리 킵니다. 스택 포인터라고 불리는 RSP 레지스터는 항상 최신 할당 된 스택 프레임 (낮은 주소)의 상단을 가리 킵니다. 아래는 프레임 스택 차트입니다.

왼쪽 열 Position 간접 주소 지정 모드를 사용하는 메모리 주소입니다. Content Position 지점에서 주소의 값입니다. 위의 차트에서 스택 프레임의 구조에 따르면, 기능 호출 절차는 다음과 같이 여러 단계로 설명 될 수있다.
2 단계와 3 단계는 실제로 call 명령에 속합니다. 또한 4 단계 및 5 단계는 다음과 같이 조립 명령에 설명 될 수 있습니다.
TestDemo`-[ViewController viewDidLoad]:
0x1054e09c0 <+0>: pushq %rbp //step 4
0x1054e09c1 <+1>: movq %rsp, %rbp //step 5
이 두 단계는 각 기능 호출과 함께 있다는 것을 쉽게 알 수 있습니다. 위의 차트에는 또 다른 세부 사항이 있습니다. RSP 레지스터 아래에 빨간색 영역이 있으며 ABI에 의해 빨간색이라고합니다. 예약되어 있으며 신호 또는 인터럽트 핸들러에 의해 수정되어서는 안됩니다. 따라서 기능 호출 중에 수정할 수 있으므로 잎 함수는 다른 기능을 호출하지 않는 기능이 임시 데이터 에이 영역을 사용할 수 있음을 의미합니다.
UIKit`-[UIViewController loadViewIfRequired]:
0x1064a63f1 <+0>: pushq %rbp
0x1064a63f2 <+1>: movq %rsp, %rbp
0x1064a63f5 <+4>: pushq %r15
0x1064a63f7 <+6>: pushq %r14
0x1064a63f9 <+8>: pushq %r13
0x1064a63fb <+10>: pushq %r12
0x1064a63fd <+12>: pushq %rbx
위의 명령 중에서 0x1064a63f5 에서 0x1064a63fd 의 명령은 6 단계에 속합니다. 함수 Preserve Register라는 일종의 레지스터가 있습니다. 즉, 호출 함수에 속하는 것을 의미하지만 값을 보존하기 위해 호출 된 함수는 필요합니다. 아래 어셈블리 지침에서 RBX, RSP 및 R12 -R15는 모두 그러한 레지스터에 속합니다.
0x1064a6c4b <+2138>: addq $0x1f8, %rsp ; imm = 0x1F8
0x1064a6c52 <+2145>: popq %rbx
0x1064a6c53 <+2146>: popq %r12
0x1064a6c55 <+2148>: popq %r13
0x1064a6c57 <+2150>: popq %r14
0x1064a6c59 <+2152>: popq %r15
0x1064a6c5b <+2154>: popq %rbp
0x1064a6c5c <+2155>: retq
0x1064a6c5d <+2156>: callq 0x106d69e9c ; symbol stub for: __stack_chk_fail
함수를 호출하는 지침은 call 입니다. 아래를 참조하십시오.
call function
매개 변수의 function 는 텍스트 세그먼트의 절차입니다. Call 명령은 두 단계로 나눌 수 있습니다. 첫 번째 단계는 스택에서 call 명령의 다음 명령어 주소를 푸시하는 것입니다. 여기서 다음 주소는 실제로 호출 된 함수가 완료된 후 리턴 주소입니다. 두 번째 단계는 function 으로 점프하는 것입니다. call 명령은 두 가지 지침보다 동일합니다.
push next_instruction
jmp function
다음은 iOS 시뮬레이터의 call 명령의 예입니다.
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
위의 코드는 call 명령의 두 가지 사용을 보여줍니다. 첫 번째 사용에서, 피연산자는 실제로 Mach-O 파일의 기호 스터브 인 메모리 주소입니다. 동적 링커를 통해 함수의 상징을 검색 할 수 있습니다. 두 번째 사용에서, 피연산자는 실제로 간접 주소 지정 모드로 얻습니다. 또한 AT & T 구문에서 * 점프/통화 명령어 (또는 프로그래머 카운터와 관련된 점프)의 즉시 피연산자에 접두사로 추가해야합니다.
일반적으로 ret 명령은 호출 함수에서 호출 함수로 절차를 반환하는 데 사용됩니다. 이 명령어는 스택 상단에서 주소를 팝하고 해당 주소로 돌아가서 계속 실행됩니다. 위의 예에서는 next_instruction 으로 돌아갑니다. ret 명령이 실행되기 전에 레지스터는 호출 함수에 속합니다. 이것은 이미 기능 호출 절차의 6 단계에서 언급되어 있습니다.
대부분의 함수에는 정수, 플로트, 포인터 등이 될 수있는 매개 변수가 있습니다. 게다가, 함수는 일반적으로 실행 결과가 성공하거나 실패했음을 나타낼 수있는 반환 값을 가지고 있습니다. OSX에서, 최대 6 개의 매개 변수는 RDI, RSI, RDX, RCX, R8 및 R9 인 레지스터를 순서대로 통과 할 수있다. 6 개 이상의 매개 변수가있는 함수는 어떻습니까? 물론,이 상황은 존재합니다. 이 경우 스택을 사용하여 나머지 매개 변수를 반전 순서로 보존 할 수 있습니다. OSX에는 8 개의 플로트 매개 변수를 전달할 수있는 8 개의 부동 소수점 레지스터가 있습니다.
함수의 반환 값에 대해 rax 레지스터는 정수 리턴 값을 저장하는 데 사용됩니다. 반환 값이 플로트 인 경우 xmm0 -xmm1 레지스터를 사용해야합니다. 아래 차트는 기능 호출 중 레지스터 사용 규칙을 명확하게 보여줍니다.

preserved across function calls 레지스터가 함수 호출에 걸쳐 보존되어야하는지 여부를 나타냅니다. RBX, R12 -R15 레지스터 외에도 위에서 언급 한 RSP 및 RBP 레지스터는 Callee -Saved Registers에도 속한다는 것을 알 수 있습니다. 이 두 레지스터는 프로그램 스택을 가리키는 중요한 위치 포인터를 예약하기 때문입니다.
다음으로 기능 호출의 지침을 보여주기 위해 실제 예제를 따릅니다. CocoaLumberjack 에서 매크로 DDLogError 예로 들어 보겠습니다. 이 매크로가 호출되면 클래스 메소드 log:level:flag:context:file:function:line:tag:format: 호출됩니다. 다음 코드 및 지침은 DDLogError 의 호출 및 해당 조립 지침에 관한 것입니다.
- (IBAction)test:(id)sender {
DDLogError(@"TestDDLog:%@", sender);
}
0x102c568a3 <+99>: xorl %edx, %edx
0x102c568a5 <+101>: movl $0x1, %eax
0x102c568aa <+106>: movl %eax, %r8d
0x102c568ad <+109>: xorl %eax, %eax
0x102c568af <+111>: movl %eax, %r9d
0x102c568b2 <+114>: leaq 0x2a016(%rip), %rcx ; "/Users/dev-aozhimin/Desktop/TestDDLog/TestDDLog/ViewController.m"
0x102c568b9 <+121>: leaq 0x2a050(%rip), %rsi ; "-[ViewController test:]"
0x102c568c0 <+128>: movl $0x22, %eax
0x102c568c5 <+133>: movl %eax, %edi
0x102c568c7 <+135>: leaq 0x2dce2(%rip), %r10 ; @"eTestDDLog:%@"
0x102c568ce <+142>: movq 0x33adb(%rip), %r11 ; (void *)0x0000000102c8ad18: DDLog
0x102c568d5 <+149>: movq 0x34694(%rip), %rbx ; ddLogLevel
0x102c568dc <+156>: movq -0x30(%rbp), %r14
0x102c568e0 <+160>: movq 0x332f9(%rip), %r15 ; "log:level:flag:context:file:function:line:tag:format:"
0x102c568e7 <+167>: movq %rdi, -0x48(%rbp)
0x102c568eb <+171>: movq %r11, %rdi
0x102c568ee <+174>: movq %rsi, -0x50(%rbp)
0x102c568f2 <+178>: movq %r15, %rsi
0x102c568f5 <+181>: movq %rcx, -0x58(%rbp)
0x102c568f9 <+185>: movq %rbx, %rcx
0x102c568fc <+188>: movq -0x58(%rbp), %r11
0x102c56900 <+192>: movq %r11, (%rsp)
0x102c56904 <+196>: movq -0x50(%rbp), %rbx
0x102c56908 <+200>: movq %rbx, 0x8(%rsp)
0x102c5690d <+205>: movq $0x22, 0x10(%rsp)
0x102c56916 <+214>: movq $0x0, 0x18(%rsp)
0x102c5691f <+223>: movq %r10, 0x20(%rsp)
0x102c56924 <+228>: movq %r14, 0x28(%rsp)
0x102c56929 <+233>: movb $0x0, %al
0x102c5692b <+235>: callq 0x102c7d2be ; symbol stub for: objc_msgSend
Objective-C의 모든 함수는 objc_msgSend 함수의 호출로 전환되므로 log:level:flag:context:file:function:line:tag:format: 메소드가 아래 코드로 바뀝니다.
objc_msgSend(DDLog, @selector(log:level:flag:context:file:function:line:tag:format:), asynchronous, level, flag, context, file, function, line, tag, format, sender)
우리는 이미 최대 6 개의 레지스터를 매개 변수 전달에 사용될 수 있다고 언급했습니다. 초과 매개 변수는 스택을 사용하여 통과를 수행 할 수 있습니다. 위의 기능에는 6 개 이상의 매개 변수가 있으므로 전달되는 매개 변수는 레지스터와 스택을 모두 사용합니다. 두 개의 테이블 아래에서 DDLogError 함수 호출의 매개 변수 전달에 대한 레지스터 및 스택의 세부 사용을 설명합니다.
| 일반 등록 | 값 | 매개 변수 | 어셈블리 지침 | 논평 |
|---|---|---|---|---|
| RDI | ddlog | 본인 | 0x102c568eb <+171> : movq %r11, %rdi | |
| RSI | "로그 : 레벨 : 플래그 : 컨텍스트 : 파일 : 함수 : 행 : 태그 : 형식 :" | OP | 0x102c568f2 <+178> : movq %r15, %rsi | |
| RDX | 0 | 비동기 | 0x102c568a3 <+99> : xorl %edx, %edx | Xorl은 독점 또는 운영입니다. 여기에서는 EDX 레지스터를 지우는 데 사용됩니다 |
| RCX | 18446744073709551615 | 수준 | 0x102c568f9 <+185> : movq %rbx, %rcx | (ddloglevelall 또는 nsuintegermax) |
| R8 | 1 | 깃발 | 0x102c568aa <+106> : movl %eax, %r8d | ddlogflagerror |
| R9 | 0 | 문맥 | 0x102c568af <+111> : movl %eax, %r9d |
| 스택 프레임 오프셋 | 값 | 매개 변수 | 어셈블리 지침 | 논평 |
|---|---|---|---|---|
| (%RSP) | "/users/dev-aozhimin/desktop/testddlog/testddlog/viewcontroller.m" | 파일 | 0x102c56900 <+192> : movq %r11, ( %rsp) | |
| 0x8 (%RSP) | "-[ViewController Test :]" | 기능 | 0x102c56908 <+200> : movq %rbx, 0x8 ( %rsp) | |
| 0x10 (%RSP) | 0x22 | 선 | 0x102c5690d <+205> : movq $ 0x22, 0x10 (%rsp) | ddlogerror의 해당 호출은 34 행에 있습니다 |
| 0x18 (%RSP) | 0x0 | 꼬리표 | 0x102c56916 <+214> : movq $ 0x0, 0x18 (%rsp) | 무 |
| 0x20 (%RSP) | "testddlog :%@" | 체재 | 0x102c5691f <+223> : movq %r10, 0x20 ( %rsp) | |
| 0x28 (%RSP) | 보내는 사람 | 변수 매개 변수의 첫 번째 매개 변수 | 0x102c56924 <+228> : movq %r14, 0x28 ( %rsp) | Uibutton의 사례 |
레지스터 값이
rsi레지스터의op매개 변수와 같은 문자열 인 경우 문자열은po (char *) $rsi명령을 통해 lldb에서 직접 인쇄 할 수 있습니다. 그렇지 않으면,po $rsi사용하여 정수 형식으로 값을 인쇄 할 수 있습니다.
어셈블리 언어의 도움으로 디버깅 중에 매우 필요한 수준의 지식을 조사 할 수 있습니다. 나는 가능한 한 상세한 어셈블리 관련 지식을 소개하기 위해 매우 열심히 노력합니다. 그러나 어셈블리의 지식 계층은 하나의 기사에서 설명하기에는 너무 엄청납니다. 위에서 언급 한 참조를 참조하십시오. 또한 CSAPP 의 세 번째 장 - 프로그램의 기계 레벨 표현도 적극 권장됩니다. 참조를위한 희귀 한 좋은 자료입니다.
이 기사는 실제 사례를 통해 디버깅 절차를 보여줍니다. 개인 정보를 보호하기 위해 일부 세부 사항이 변경되었습니다.
우리가 이야기 할 문제는 로그인 SDK를 개발할 때 일어나고있었습니다. 한 사용자는 로그인 페이지에서 "QQ"버튼을 누를 때 앱이 충돌했다고 주장했습니다. 이 문제를 디버깅하면서 QQ 앱이 동시에 설치되지 않은 경우 충돌이 발생했습니다. 사용자가 QQ 버튼을 눌러 로그인이 필요한 경우 QQ 로그인 SDK는 앱에서 인증 웹 페이지를 시작하려고 시도합니다. 이 경우 인식되지 않은 선택기 오류 [TCWebViewController setRequestURLStr:] 가 발생합니다.
추신 :이 문제에 중점을두기 위해 불필요한 비즈니스 디버그 정보는 아래에 나열되어 있지 않습니다. 한편 Aadebug는 앱 이름으로 사용됩니다.
이 충돌의 스택 추적은 다음과 같습니다.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TCWebViewController setRequestURLStr:]: unrecognized selector sent to instance 0x7fe25bd84f90'
*** First throw call stack:
(
0 CoreFoundation 0x0000000112ce4f65 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x00000001125f7deb objc_exception_throw + 48
2 CoreFoundation 0x0000000112ced58d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 AADebug 0x0000000108cffefc __ASPECTS_ARE_BEING_CALLED__ + 6172
4 CoreFoundation 0x0000000112c3ad97 ___forwarding___ + 487
5 CoreFoundation 0x0000000112c3ab28 _CF_forwarding_prep_0 + 120
6 AADebug 0x000000010a663100 -[TCWebViewKit open] + 387
7 AADebug 0x000000010a6608d0 -[TCLoginViewKit loadReqURL:webTitle:delegate:] + 175
8 AADebug 0x000000010a660810 -[TCLoginViewKit openWithExtraParams:] + 729
9 AADebug 0x000000010a66c45e -[TencentOAuth authorizeWithTencentAppAuthInSafari:permissions:andExtraParams:delegate:] + 701
10 AADebug 0x000000010a66d433 -[TencentOAuth authorizeWithPermissions:andExtraParams:delegate:inSafari:] + 564
………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………
Lines of irrelevant information are removed here
………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………
236
14 libdispatch.dylib 0x0000000113e28ef9 _dispatch_call_block_and_release + 12
15 libdispatch.dylib 0x0000000113e4949b _dispatch_client_callout + 8
16 libdispatch.dylib 0x0000000113e3134b _dispatch_main_queue_callback_4CF + 1738
17 CoreFoundation 0x0000000112c453e9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
18 CoreFoundation 0x0000000112c06939 __CFRunLoopRun + 2073
19 CoreFoundation 0x0000000112c05e98 CFRunLoopRunSpecific + 488
20 GraphicsServices 0x0000000114a13ad2 GSEventRunModal + 161
21 UIKit 0x0000000110d3f676 UIApplicationMain + 171
22 AADebug 0x0000000108596d3f main + 111
23 libdyld.dylib 0x0000000113e7d92d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
디버깅에 대해 이야기하기 전에 Objective-C의 메시지 전달에 익숙해 봅시다. 우리가 알고 있듯이 Objective-C는 기능 호출보다는 메시징 구조를 사용합니다. 주요 차이점은 메시징 구조에서 런타임이 컴파일 시간이 아닌 어떤 기능을 실행할 것인지 결정한다는 것입니다. 즉, 인식되지 않은 메시지가 하나의 객체로 전송되면 컴파일 시간 동안 아무 일도 일어나지 않습니다. 그리고 런타임 동안 이해하지 못하는 메소드를 수신 할 때 객체는 메시지 전달을 통해 개발자로서 알려지지 않은 메시지를 처리하는 방법을 알려줄 수 있도록 설계된 프로세스입니다.
메시지 전달 중에 일반적으로 4 가지 방법이 포함됩니다.
+ (BOOL)resolveInstanceMethod:(SEL)sel :이 메소드는 알 수없는 메시지가 객체로 전달 될 때 호출됩니다. 이 방법은 찾을 수없는 선택기를 가져 와서 부울 값을 반환하여 인스턴스 메소드가 해당 선택기를 처리 할 수있는 클래스에 추가되었는지 여부를 나타냅니다. 클래스 가이 선택기를 처리 할 수 있다면 예를 반환하면 메시지 전달 프로세스가 완료됩니다. 이 방법은 종종 Coredata에서 nsmanagedobjects의 @dynamic 속성에 동적으로 접근하는 데 사용됩니다. + (BOOL)resolveClassMethod:(SEL)sel 메소드는 위 메소드와 유사합니다. 유일한 차이점은이 클래스 메소드이고 다른 클래스는 인스턴스 메소드입니다.
- (id)forwardingTargetForSelector:(SEL)aSelector :이 방법은 알 수없는 메시지를 처리하기위한 두 번째 수신기를 제공하며 forwardInvocation: 이 방법은 다중 상속의 일부 기능을 모방하는 데 사용될 수 있습니다. 전달 경로 의이 부분을 사용하여 메시지를 조작 할 방법이 없습니다. 교체 수신기로 보내기 전에 메시지를 변경 해야하는 경우 전체 전달 메커니즘을 사용해야합니다.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector : 전달 알고리즘이 지금까지 오면 전체 전달 메커니즘이 시작됩니다. NSMethodSignature 이 메소드에 의해 반환됩니다. 여기에는 Aselector 매개 변수의 메소드 설명이 포함됩니다. 메시지 전달 중에 선택기, 대상 및 인수를 포함하는 NSInvocation 객체를 만들려면이 메소드를 재정의해야합니다.
- (void)forwardInvocation:(NSInvocation *)anInvocation :이 방법의 구현에는 아래 부분이 포함되어야합니다. 해당 객체에 메시지를 보내면 aninvocation은 반환 값을 저장하고 런타임을 저장 한 다음 원래 메시지 발신자에게 반환 값을 보냅니다. 실제로,이 방법은 forwardingTargetForSelector: 와 동일한 동작을 가질 수 있습니다.
일반적으로 메시지 전달에 사용되는 처음 두 가지 방법을 빠른 전달 이라고합니다. 메시지 전달을 수행하는 훨씬 빠른 방법을 제공하기 때문입니다. 빠른 전달과 구별하기 위해 방법 3 및 4를 정상 전달 또는 일반 전달 이라고합니다. 메시지 전달을 완료하기 위해 nsinvocation 객체를 만들어야하기 때문에 훨씬 느립니다.
참고 :
methodSignatureForSelector메소드가 재정의되지 않거나 반환 된NSMethodSignature가 NIL 인 경우forwardInvocation호출되지 않으며 메시지 전달이doesNotRecognizeSelector오류가 발생하지 않습니다. 아래__forwarding__함수의 소스 코드에서 볼 수 있습니다.
메시지 전달 과정은 흐름도에서 설명 할 수 있습니다 (아래 참조).

플로우 다이어그램에 설명 된 것처럼 각 단계에서 수신기는 메시지를 처리 할 기회가 주어집니다. 각 단계는 이전 단계보다 비쌉니다. 모범 사례는 가능한 빨리 메시지 전달 프로세스를 처리하는 것입니다. 전체 프로세스를 통해 메시지가 처리되지 않으면, 선택기를 객체에 의해 인식 할 수 없도록하는 데있어서 인식 할 수 doesNotRecognizeSeletor 오류가 발생하지 않습니다.
이제 이론 부분을 완료하고 문제로 돌아갈 시간입니다.
Trace Stack의 TCWebViewController 정보에 따르면, 우리는 자연스럽게 Tencent SDK TencentoPenapi.framework 와 연결하지만 최근 Tencent SDK를 업데이트하지 않았으므로 TencentoPenapi.framework 에 의해 충돌이 발생하지 않았 음을 의미합니다.
먼저 코드를 분해하고 TCWebViewController 클래스의 구조물을 얻었습니다.
@class TCWebViewController : UIViewController<UIWebViewDelegate, NSURLConnectionDelegate, NSURLConnectionDataDelegate> {
@property webview
@property webTitle
@property requestURLStr
@property error
@property delegate
@property activityIndicatorView
@property finished
@property theData
@property retryCount
@property hash
@property superclass
@property description
@property debugDescription
ivar _nloadCount
ivar _webview
ivar _webTitle
ivar _requestURLStr
ivar _error
ivar _delegate
ivar _xo
ivar _activityIndicatorView
ivar _finished
ivar _theData
ivar _retryCount
-setError:
-initWithNibName:bundle:
-dealloc
-stopLoad
-doClose
-viewDidLoad
-loadReqURL
-viewDidDisappear:
-shouldAutorotateToInterfaceOrientation:
-supportedInterfaceOrientations
-shouldAutorotate
-webViewDidStartLoad:
-webViewDidFinishLoad:
-webView:didFailLoadWithError:
-webView:shouldStartLoadWithRequest:navigationType:
}
정적 분석 결과로부터 TCWebViewController 에 Setter 및 Getter requestURLStr 가 없었습니다. 이전 앱 버전에는 충돌이 없었기 때문에 아이디어가 나왔습니다. TCWebViewController 의 속성이 @dynamic 사용하여 컴파일러가 컴파일 시간 동안 속성에 대한 Getter 및 Setter를 생성하지 않고 핵심 데이터 프레임 워크와 같은 런타임에서 동적으로 생성되는 동적 방식으로 구현되었을 것입니까? 그런 다음 우리는 우리의 추측이 옳은지 확인하기 위해 아이디어를 깊이 가기로 결정했습니다. 추적하는 동안 TencentoPenapi.framework 의 NSObject 에 대한 카테고리 NSObject(MethodSwizzlingCategory) 가 매우 의심 스러웠습니다. 이 범주에는 QQmethodSignatureForSelector 및 QQforwardInvocation 메서드의 methodSignatureForSelector 및 forwardInvocation 메소드를 대체하는 메소드 switchMethodForCodeZipper 퍼가있었습니다.
void +[ NSObject switchMethodForCodeZipper ]( void * self, void * _cmd) {
rbx = self;
objc_sync_enter (self);
if (*( int8_t *)_g_instance == 0x0 ) {
[ NSObject swizzleMethod: @selector ( methodSignatureForSelector: ) withMethod: @selector ( QQmethodSignatureForSelector: )];
[ NSObject swizzleMethod: @selector ( forwardInvocation: ) withMethod: @selector ( QQforwardInvocation: )];
*( int8_t *)_g_instance = 0x1 ;
}
rdi = rbx;
objc_sync_exit (rdi);
return ;
} 그런 다음 QQmethodSignatureForSelector 메소드를 계속 추적했으며 _AddDynamicPropertysSetterAndGetter 라는 메소드가있었습니다. 이름에서, 우리는이 방법이 속성에 대한 세터와 getter 메소드를 동적으로 추가하는 것임을 쉽게 얻을 수 있습니다. 이것은 우리의 원래 추측이 정확하다는 것을 실질적으로 확인할 수 있습니다.
void * -[ NSObject QQmethodSignatureForSelector: ]( void * self, void * _cmd, void * arg2) {
r14 = arg2;
rbx = self;
rax = [ self QQmethodSignatureForSelector: rdx];
if (rax == 0x0 ) {
rax = sel_getName (r14);
_AddDynamicPropertysSetterAndGetter ();
rax = 0x0 ;
if ( 0x0 != 0x0 ) {
rax = [rbx methodSignatureForSelector: r14];
}
}
return rax;
} 그러나 왜 TCWebViewController 클래스에서 세터가 인식 할 수 없습니까? 이 버전을 개발하는 동안 QQMethodSignatureForSelector 메소드가 다루어 졌기 때문입니까? 그러나 우리는 단서를 찾을 수 없었습니다. 그것은 매우 실망 스러웠습니다. 지금까지 정적 분석이 완료되었습니다. 다음 단계는 LLDB를 사용하여 Tencent SDK를 동적으로 디버깅하여 메시지 전달 프로세스에서 Getter와 Setter의 생성을 깨뜨리는 경로를 찾습니다.
lldb 명령을 통해
setRequestURLStr에서 중단 점을 설정하려고하면 그것을 만들 수 없다는 것을 알게 될 것입니다. 그 이유는 컴파일 시간 동안 세터를 사용할 수 없기 때문입니다. 이것은 또한 우리의 원래 추측을 확인할 수 있습니다.
Crash Stack Trace에 따르면 setRequestURLStr 가 -[TCWebViewKit open] 메소드에서 호출되도록 결론을 내릴 수 있습니다. 즉, Tencent SDK를 확인하는 동안 QQ 앱이 설치되어 있는지, 인증 웹 페이지 진행 상황을 여는 동안 충돌이 발생합니다.
그런 다음 아래 LLDB 명령을 사용 하여이 메소드에서 중단 점을 설정합니다.
br s -n "-[TCWebViewKit open]"
br sbr s -Fbreakpoint set에 대한 약어이며,-n그 후 메소드 이름에 따라 중단 점을 설정합니다.b -[TCWebViewKit open]도 여기서 작동하지만 여기서b정기적 인 표현식을 사용하여 중단 점을 설정하는_regexp-break의 약어입니다. 결국, 우리는 또한br s -a 0x000000010940b24e와 같은 메모리 주소에서 중단 점을 설정할 수 있습니다. 이는 블록의 주소를 사용할 수있는 경우 블록을 디버그하는 데 도움이 될 수 있습니다.
지금까지 중단 점은 성공적으로 설정됩니다.
Breakpoint 34: where = AADebug`-[TCWebViewKit open], address = 0x0000000103157f7d
App이 웹 인증 페이지를 시작할 때이 중단 점에서 프로젝트가 중지됩니다. 아래를 참조하십시오 :

이 스크린 샷은 시뮬레이터에서 앱을 실행할 때 캡처되므로 어셈블리 코드는 x64를 기반으로합니다. iPhone 장치를 사용하는 경우 어셈블리 코드가 ARM이어야합니다. 그러나 분석 방법은 동일합니다.
96 행에서 중단 점을 설정하십시오.이 어셈블리 코드는 setRequestURLStr 메소드 호출 인 다음 rbx 레지스터의 내용을 인쇄 한 다음 TCWebViewController 인스턴스 가이 레지스터에 저장되었음을 알 수 있습니다.

다음으로 LLDB를 사용하여 QQmethodSignatureForSelector 메소드의 중단 점을 설정할 수 있습니다.
br s -n "-[NSObject QQmethodSignatureForSelector:]"
LLDB에 c 입력하려면 중단 점을 계속 유지하면 중단 점이 QQmethodSignatureForSelector 메서드 내부에서 중단됩니다. 이는 코드와 충돌하는 QQmethodSignatureForSelector 메소드에 대한 이전 추측을 증명할 수 있습니다.

QQmethodSignatureForSelector 메소드의 끝에서 중단 점을 설정하십시오. 즉, 31 행의 retq 명령입니다. 그런 다음 레지스터 rax 의 메모리 주소를 인쇄하고 아래 스크린 샷을 참조하십시오.

레지스터 rax 의 메모리 주소 0x00007fdb36d38df0 인쇄하여 NSMethodSignature 객체가 반환됩니다. X86 어셈블리 언어에 대한 설계 규칙에 따르면, 리턴 값은 레지스터 rax 에 저장됩니다. 분명히 QQmethodSignatureForSelector 메소드가 호출되어 올바른 값을 반환하므로 문제를 계속 추적해야합니다.
lldb를 통해 QQforwardInvocation 에서 중단 점을 설정하십시오.
br s -n "-[NSObject QQforwardInvocation:]"
중단 점이 설정된 후 프로그램 실행을 계속하면 앱이 충돌합니다. QQforwardInvocation 방법은 아직 호출되지 않았습니다. 이를 통해 QQforwardInvocation 방법이 코드에 의해 충돌한다고 결론을 내릴 수 있습니다.

___forwarding___ 함수에는 메시지 전달 메커니즘의 전체 구현이 포함되어 있으며, 대결 코드는 Objective-C 消息发送与转发机制原理에서 선택됩니다. 이 기사에는 forwardingTargetForSelector 메소드를 호출 할 때 forwarding 과 receiver 에 잘못된 판단이 있습니다. 여기서는 forwardingTarget 과 receiver 사이의 판단이어야합니다. 아래 코드를 참조하십시오.
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// call forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// Zombie Object
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
// call methodSignatureForSelector first to get method signature , then call forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// if selector already registered in Runtime
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}
기본적으로 소환 코드를 읽음으로써 명확한 이해를 가질 수 있습니다. 먼저 메시지 전달 프로세스 중에 forwardingTargetForSelector 메소드를 호출하여 교체 수신기를 얻기 위해 빠른 전달 단계라고도합니다. forwardingTarget NIL을 반환하거나 동일한 수신기를 반환하면 메시지 전달이 일반 전달 단계로 바뀝니다. 기본적으로 메소드 서명을 가져 오기 위해 methodSignatureForSelector 메서드를 호출 한 다음 frameStackPointer 와 함께 사용하여 invocation 객체를 인스턴스화합니다. 그런 다음 receiver 의 Methods를 forwardInvocation: 하고 이전 invocation 객체를 인수로 전달하십시오. 결국 methodSignatureForSelector 메소드가 구현되지 않고 selector 이미 런타임 시스템에 등록되어 있으면 오류를 던지기 위해 호출되지 doesNotRecognizeSelector: .
충돌 스택 추적에서 ___forwarding___ 면밀히 조사하면 전체 메시지 전달 경로 중 두 번째 경로 forwardInvocation 불린다는 NSInvocation 알 수 있습니다.
또한 중단 점 후 단계별로 명령을 실행하여 어셈블리 코드의 실행 경로를 관찰 할 수 있습니다. 동일한 결과를 관찰해야합니다.

그리고 forwardInvocation 이 호출 될 때 어떤 방법이 실행됩니까? 스택 추적에서 __ASPECTS_ARE_BEING_CALLED__ 라는 메소드가 실행되는 것을 볼 수 있습니다. 전체 프로젝트 의이 방법을 살펴보면 마침내 forwardInvocation Aspects 프레임 워크에 의해 연결되어 있음을 알게됩니다.
static void aspect_swizzleForwardInvocation ( Class klass) {
NSCParameterAssert (klass);
// If there is no method, replace will act like class_addMethod.
IMP originalImplementation = class_replaceMethod (klass, @selector ( forwardInvocation: ), ( IMP )__ASPECTS_ARE_BEING_CALLED__, " v@:@ " );
if (originalImplementation) {
class_addMethod (klass, NSSelectorFromString (AspectsForwardInvocationSelectorName), originalImplementation, " v@:@ " );
}
AspectLog ( @" Aspects: %@ is now aspect aware. " , NSStringFromClass (klass));
} // This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__ (__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
NSLog ( @" selector: %@ " , NSStringFromSelector (invocation. selector ));
NSCParameterAssert (self);
NSCParameterAssert (invocation);
SEL originalSelector = invocation. selector ;
SEL aliasSelector = aspect_aliasForSelector (invocation. selector );
invocation. selector = aliasSelector;
AspectsContainer *objectContainer = objc_getAssociatedObject (self, aliasSelector);
AspectsContainer *classContainer = aspect_getContainerForClass ( object_getClass (self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc ] initWithInstance: self invocation: invocation];
NSArray *aspectsToRemove = nil ;
// Before hooks.
aspect_invoke (classContainer. beforeAspects , info);
aspect_invoke (objectContainer. beforeAspects , info);
// Instead hooks.
BOOL respondsToAlias = YES ;
if (objectContainer. insteadAspects . count || classContainer. insteadAspects . count ) {
aspect_invoke (classContainer. insteadAspects , info);
aspect_invoke (objectContainer. insteadAspects , info);
} else {
Class klass = object_getClass (invocation. target );
do {
if ((respondsToAlias = [klass instancesRespondToSelector: aliasSelector])) {
[invocation invoke ];
break ;
}
} while (!respondsToAlias && (klass = class_getSuperclass (klass)));
}
// After hooks.
aspect_invoke (classContainer. afterAspects , info);
aspect_invoke (objectContainer. afterAspects , info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {
invocation. selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString (AspectsForwardInvocationSelectorName);
if ([ self respondsToSelector: originalForwardInvocationSEL]) {
(( void ( *)( id , SEL , NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
} else {
[ self doesNotRecognizeSelector: invocation.selector];
}
}
// Remove any hooks that are queued for deregistration.
[aspectsToRemove makeObjectsPerformSelector: @selector ( remove )];
} TCWebViewController 는 Tencent SDK의 개인 클래스이므로 다른 클래스에 직접 연결되지는 않습니다. 그러나이 클래스에도 영향을 줄 수있는 슈퍼 클래스가 연결되어있을 수 있습니다. 이 추측으로 우리는 계속 파고 들었습니다. 미세하게 대답이 나타났습니다! UIViewController 고치는 코드를 제거하거나 댓글을 달면 QQ를 통해 로그인 할 때 앱이 충돌하지 않았습니다. 지금까지 우리는 충돌이 Aspects 프레임 워크에 의해 관련되었다고 확신했습니다.

doesNotRecognizeSelector: error is thrown by __ASPECTS_ARE_BEING_CALLED__ method which is used to replace the IMP of forwardInvocation: method by Aspects . The implementation of __ASPECTS_ARE_BEING_CALLED__ method has the corresponding time slice for before, instead and after the hooking in Aspect . Among above code, aliasSelector is a SEL which is handled by Aspects , like aspects__setRequestURLStr: .
In Instead hooks part, invocation.target will be checked if it can respond to aliasSelector. If subclass cannot respond, the superclass will be checked, the superclass's superclass, until root class. Since the aliasSelector cannot be responded, respondsToAlias is false. Then originalSelector is assigned to be a selector of invocation. Next objc_msgSend invokes the invocation to call the original SEL. Since TCWebViewController cannot respond the originalSelector:setRequestURLStr: method, it finally runs to ASPECTS_ARE_BEING_CALLED method of Aspects and doesNotRecognizeSelector: method is threw accordingly, which is the root cause of the crash we talked about in the beginning of this article.
Some careful reader might already realize the crash could be involved with Aspects, since seeing line ASPECTS_ARE_BEING_CALLED at line 3 of the crash stack trace. The reason I still listed all the attempts here is that I hope you can learn how to locate a problem from a third-part framework without source code through static analysis and dynamic analysis. Hope the tricks and technology mentioned in this article can be helpful for you.
There are two available ways to fix the crash. One is hooking the method of Aspects which is less invasive, for example Method Swizzling, then the setter creation during the message forwarding process for TencentOpenAPI would not be interrupted. Another is replace forwardInvocation: with ours implementation, if both aliasSelector and ``originalSelector cannot response to the message forwarding, we can forward the message forwarding path back into the original path. Refer to the code below:
if (!respondsToAlias) {
invocation. selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString (AspectsForwardInvocationSelectorName);
(( void ( *)( id , SEL , NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
} In fact, Aspects has conflicts with JSPatch . Since the implementation of these two SDK are similar too, doesNotRecognizeSelector: happens too when they are used together. Please Refer to 微信读书的文章.
The root cause of this crash is the conflict between Aspects and TencentOpenAPI frameworks. The life cycle method of UIViewController class is hooked by Aspects, and the forwardInvocation method is replaced with the Aspects's implementation. Also, because of the superclass of TCWebViewController is UIViewController class. As a result, QQforwardInvocation method of TCWebViewController class is hooked by Aspects too. That leads to the message forwarding process failed, thus, the creation of getter and setter fails too.
This case tells us, we should not only learn how to use a third-part framework, but also need to look into the mechanism of it. Only then, we can easily to locate the problem we meet during our work.
We introduce different kinds of tips in this article, but we hope you can also master a way of thinking when debugging. Skills are easy to be learned, but the way you think when resolving problem is not easy to be formed. It takes time and practice. Besides kinds of debugging techniques, you also have to have a good sense of problem analysis, then the problem will be handy for you.
Special thanks to below readers, I really appreciate your support and valuable suggestions.