Objective-C를 처음 접했을 때 클래스 메서드를 호출하였을 때 C의 함수 호출과 다른점이 무엇일까? 라는 생각이 들었는데.. 그냥 wrapping 한건지 아니면 뭐 또 다른게 있을지 말이죠.
그 동안 귀찮아서 미루다가 오늘 disassemble 해 보았습니다. 테스트 환경은 iPhone Simulator입니다. 따라서 레지스터 이름들이 익숙한(?) eax, ebx가 나오는데요.. 실제 iPhone은 arm이라 어떻게 disassemble 될 지는 잘 모르겠습니다;; (제가 disasm 초보라 양해 부탁드립니다~)
Dump of assembler code for function Get:
0x00001ee4 : nop
0x00001ee5 : nop
0x00001ee6 : nop
0x00001ee7 : nop
0x00001ee8 : nop
0x00001ee9 : nop
0x00001eea : push ebp
0x00001eeb : mov ebp,esp
0x00001eed : sub esp,0x8
0x00001ef0 : mov eax,0x1e
0x00001ef5 : leave
0x00001ef6 : ret
이것 역시 완전히 동일합니다. mac 개발에서도 eax로 리턴값을 주는군요.
이번에는 메서드를 호출하였을 경우 어떻게 코드가 실행되는지 분석 해 보겠습니다.
1. [myClass setMember:10] 호출
0x00001e67 : mov edx,DWORD PTR [ebp-0xc] // myClass 주소 ($ebp-0x0c)를 edx에 저장
0x00001e6a : lea eax,[ebx+0x121b] // selector(setMember:)주소를 가리키고 있는 변수
0x00001e70 : mov eax,DWORD PTR [eax] // eax가 가리키는 곳의 값을 eax에 넣음
0x00001e72 : mov DWORD PTR [esp+0x8],0xa // 10을 esp+0x08에 넣음 (3번째 인자)
0x00001e7a : mov DWORD PTR [esp+0x4],eax // selector 값을 넣음 (2번째 인자)
0x00001e7e : mov DWORD PTR [esp],edx // 스택에 myClass 주소를 넣음 (1번째 인자)
0x00001e81 : call 0x4005 // dyld_stub_objc_msgSend 호출
objc_msgSend는 많이 들어 보셨을 껍니다. 잘못된 receiver에 메시지를 날릴려고 하면 항상 나오는 EXC_BAD_ACCESS와 같은 오류 상황에서 backtrace (call stack) 해보면 objc_msgSend()가 찍히죠. 여기서 보시면 receiver가 edx에 저장이 되고 eax에는 selector가 저장됩니다.[2] 이러한 값들을 c 코드로 보면 다음과 같이 되겠죠.
와 같이 나오는데, 또 다시 objc_msgSend를 거치는 것을 보실 수 있습니다. (dyld_stub_objc_msgSend는 말그대로 stub 코드라서)
3. objc_msgSend 에서는 무슨 일이?
1번에서 넣어준 인자 덕분에 objc_msgSend()는 myClass, selector, 10 이라는 3개의 인자를 받았습니다. 이 인자들을 이용해서 무엇인가를 하겠죠.
objc_msgSend()는 아래와 같이 생겼습니다. 정신건강을 위해 more/less로 처리 했습니다 :)
objc_msgSend() 보기
0x95da0670 : mov ecx,DWORD PTR [esp+0x8]
0x95da0674 : mov eax,DWORD PTR [esp+0x4]
0x95da0678 : cmp ecx,0xfffeb010
0x95da067e : je 0x95da06fb
0x95da0680 : test eax,eax
0x95da0682 : je 0x95da06e0
0x95da0684 : mov edx,DWORD PTR [eax+0x0]
0x95da0687 : push edi
0x95da0688 : mov edi,DWORD PTR [edx+0x20]
0x95da068b : push esi
0x95da068c : mov esi,DWORD PTR [edi+0x0]
0x95da068f : lea edi,[edi+0x8]
0x95da0692 : mov edx,ecx
0x95da0694 : shr edx,0x2
0x95da0697 : and edx,esi
0x95da0699 : mov eax,DWORD PTR [edi+edx*4]
0x95da069c : test eax,eax
0x95da069e : je 0x95da06aa
0x95da06a0 : cmp ecx,DWORD PTR [eax+0x0]
0x95da06a3 : je 0x95da06c0
0x95da06a5 : add edx,0x1
0x95da06a8 : jmp 0x95da0697
0x95da06aa : pop esi
0x95da06ab : pop edi
0x95da06ac : mov eax,DWORD PTR [esp+0x4]
0x95da06b0 : mov eax,DWORD PTR [eax+0x0]
0x95da06b3 : jmp 0x95da06cc
0x95da06b5 : nop DWORD PTR [eax+eax+0x0]
0x95da06ba : nop WORD PTR [eax+eax+0x0]
0x95da06c0 : mov eax,DWORD PTR [eax+0x8]
0x95da06c3 : pop esi
0x95da06c4 : pop edi
0x95da06c5 : mov edx,0x1
0x95da06ca : jmp eax
0x95da06cc : sub esp,0x4
0x95da06cf : push ecx
0x95da06d0 : push eax
0x95da06d1 : call 0x95d901e8 <_class_lookupMethodAndLoadCache>
0x95da06d6 : add esp,0xc
0x95da06d9 : mov edx,0x1
0x95da06de : jmp eax
0x95da06e0 : call 0x95da06e5
0x95da06e5 : pop edx
0x95da06e6 : mov eax,DWORD PTR [edx+0xa9a7f6b]
0x95da06ec : test eax,eax
0x95da06ee : je 0x95da06f6
0x95da06f0 : mov DWORD PTR [esp+0x4],eax
0x95da06f4 : jmp 0x95da0684
0x95da06f6 : mov edx,0x0
0x95da06fb : ret
뭐가 뭔지 잘 모르겠지만 한번 보도록 하죠. 청므에 ecx에 esp+0x08, eax에 esp+0x04를 넣습니다. 각각 selector와 myClass가 되겠죠. 이렇게 넣고 0xfffeb010과 비교를 하는데 이걸 찾아보니 ignore selector라는 군요.[4] 참고 자료를 보시면
와 같이 되어 있습니다. i386일때 selector가 0xfffeb010이면 is_ignored_selector가 true가 됩니다. 위의 어셈 코드를 보시면 아시겠지만.. 이 값을 갖게 되면 무시되서 그냥 return 하네요. (왜 이렇게 하는지 용도는 먼지 모르겠습니다??) 참고로 inline으로 선언되어 있어서 is_ignored_selector(selector)와 같이 호출하여도 어셈 코드 상에는 하드 코딩 됩니다.
receiver에게 데이터를 전달하기 위해 계속 진행하다 보면 <objc_msgSend+44>지점에서 eax가 0인지 확인 합니다. 여기서 eax는 selector인데, 만약 0이라면 <objc_msgSend+58>로 이동하겠지만 0이 아니므로 계속 진행합니다. 이렇게 쭉 가다가 "_class_lookupMethodAndLoadCache"라는 곳으로 이동하는데요, 이렇게 이동을 해서 보면 정신 없는 코드로 빠져 버립니다. 이것 역시 정신건강을 위해 less/more로..
일반 C++ 메서드 호출과는 비교할 수 없을 정도로 징하게 많이 호출하는군요; 보시면 _class_lookupMethodAndLoadCache, _class_getFreedObjectClass, _class_getNonexistentObjectClass, _class_isInitialized, class_initialize과 같은 메서드 들을 호출합니다. 정확히 분석해 보진 않았지만 receiver가 nil 일 경우 여기서 걸러지겠죠?? 여기서 상위 클래스의 메서드 호출 등을 수행하고 메서드를 cache 하는 동작을 한다고 합니다.[3] 머.. 여기선 특별한게 없는 듯 합니다.
다시 objc_msgSend()로 가서 보면 <objc_msgSend+110>에서 jmp eax 합니다. 여기서 eax는 selector 주소가 계산되어 실제 코드 영역으로 진입하게 됩니다. 이 부분을 통해 setMember:가 호출 되는 것이죠.