성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - x86 실행 환경에서 SECURITY_ATTRIBUTES 구조체를 CreateEvent에 전달할 때 예외 발생</h1> <p> 예전에 null dacl을 생성해서 테스트하는 방법에 관해 설명했었는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 커널 객체를 위한 null DACL 생성 방법 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1749'>http://www.sysnet.pe.kr/2/0/1749</a> </pre> <br /> 위의 코드를 다음과 같이 간략화해서 각각 x86, x64로 빌드해 실행해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.Win32.SafeHandles; using System; using System.Runtime.InteropServices; using System.Security.AccessControl; class Program { [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public uint nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES /* * */ lpSecurityAttributes</span>, bool isManualReset, bool initialState, string name); static void Main(string[] args) { SafeWaitHandle swh = CreateEventWithNullDacl(); } // http://codemortem.blogspot.kr/2006/01/creating-null-dacl-in-managed-code.html public static SECURITY_ATTRIBUTES GetNullDacl() { // Build NULL DACL (Allow everyone full access) RawSecurityDescriptor gsd = new RawSecurityDescriptor(ControlFlags.DiscretionaryAclPresent, null, null, null, null); // Construct SECURITY_ATTRIBUTES structure SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); sa.nLength = (uint)Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES)); sa.bInheritHandle = false; // Get binary form of the security descriptor and copy it into place byte[] desc = new byte[gsd.BinaryLength]; gsd.GetBinaryForm(desc, 0); sa.lpSecurityDescriptor = Marshal.AllocHGlobal(desc.Length); // This Alloc is Freed by the Disposer or Finalizer Marshal.Copy(desc, 0, sa.lpSecurityDescriptor, desc.Length); return sa; } private static SafeWaitHandle CreateEventWithNullDacl() { SECURITY_ATTRIBUTES sa = GetNullDacl(); SafeWaitHandle swh = CreateEvent(sa, true, false, "TEST_EVENT"); return swh; } } </pre> <br /> x64의 경우 문제가 없는 반면, x86 환경에서는 CreateEvent 호출 시 다음과 같은 예외가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > System.AccessViolationException was unhandled Message=Attempted to read or write protected memory. This is often an indication that other memory is corrupt. Source=NullDaclEvent StackTrace: at Program.CreateEvent(SECURITY_ATTRIBUTES lpSecurityAttributes, Boolean isManualReset, Boolean initialState, String name) at Program.CreateEventWithNullDacl() in C:\nulldacl\NullDaclEvent\Program.cs:line 48 at Program.Main(String[] args) in C:\nulldacl\NullDaclEvent\Program.cs:line 21 InnerException: </pre> <br /> 또한, "Enable native code debugging" 옵션을 켜 놓으면 다음과 같은 예외를 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Exception thrown at 0x77900A4B (ntdll.dll) in NullDaclEvent.exe: 0xC0000005: Access violation reading location 0x00000001. If there is a handler for this exception, the program may be safely continued. </pre> <br /> 그런데, 한번 더 생각해 보면 x64가 이상하고 x86이 정상이라는 결론이 나옵니다. 왜냐하면 CreateEvent 함수는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > CreateEvent function ; <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa'>https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa</a> </pre> <br /> 첫 번째 인자가 포인터 형이기 때문입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > HANDLE WINAPI CreateEvent( <span style='color: blue; font-weight: bold'>_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,</span> _In_ BOOL bManualReset, _In_ BOOL bInitialState, _In_opt_ LPCTSTR lpName ); </pre> <br /> 하지만 우리가 사용한 C# 코드를 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // ...[생략]... [StructLayout(LayoutKind.Sequential)] <span style='color: blue; font-weight: bold'>public struct SECURITY_ATTRIBUTES</span> { public uint nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES lpSecurityAttributes</span>, bool isManualReset, bool initialState, string name); // ...[생략]... <span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();</span> // ...[생략]... CreateEvent(sa, true, false, "TEST_EVENT"); </pre> <br /> CreateEvent에는 스택에 할당된 sa 구조체(struct) 인스턴스의 값이 통째로 넘어가게 되어 있습니다. 포인터가 아니므로 당연히 문제가 발생해야 합니다. 실제로 이것이 포인터라는 것을 명시하면 x86에서의 문제가 사라집니다. 그 방법은 대략 다음과 같은 2가지 정도가 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // ==== 1. 구조체를 Pinning하고 주소를 전달 [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static unsafe extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>IntPtr</span> lpSecurityAttributes, bool isManualReset, bool initialState, string name); GCHandle gcHandle = GCHandle.Alloc(sa, GCHandleType.Pinned); SafeWaitHandle swh = CreateEvent(<span style='color: blue; font-weight: bold'>gcHandle.AddrOfPinnedObject()</span>, true, false, "TEST_EVENT"); gcHandle.Free(); </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // ==== 2. 스택에 있는 구조체의 포인터를 직접 전달 [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static unsafe extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES *</span>lpSecurityAttributes, bool isManualReset, bool initialState, string name); <span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES* pSec = &sa;</span> SafeWaitHandle swh = CreateEvent(pSec, true, false, "TEST_EVENT"); </pre> <br /> <hr style='width: 50%' /><br /> <br /> 자, 그럼 왜 x86에서 문제가 발생했는지 추적해 보겠습니다. 이를 위해 정상적으로 x86에서 실행될 때의 환경을 살펴볼 텐데요, 다음은 위에서 소개한 2번 방법을 적용한 코드입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.Win32.SafeHandles; using System; using System.Runtime.InteropServices; using System.Security.AccessControl; class Program { [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public uint nLength; public IntPtr lpSecurityDescriptor; public int bInheritHandle; } // ...[생략]... [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static unsafe extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES* lpSecurityAttributes</span>, bool isManualReset, bool initialState, string name); // http://codemortem.blogspot.kr/2006/01/creating-null-dacl-in-managed-code.html private static unsafe SafeWaitHandle CreateEventWithNullDacl() { // ...[생략]... <span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES* pSec = &sa;</span> SafeWaitHandle swh = <span style='color: blue; font-weight: bold'>CreateEvent(pSec, true, false, "TEST_EVENT");</span> return swh; } } </pre> <br /> Visual Studio에서 디버깅을 시작해 disassembly 창으로 CreateEvent의 호출 코드를 확인하니 다음과 같이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > SafeWaitHandle swh = CreateEvent(pSec, true, false, "TEST_EVENT"); 050701EF push 0 050701F1 push dword ptr ds:[3CC2030h] 050701F7 mov ecx,dword ptr [ebp-50h] 050701FA mov edx,1 050701FF call 00DEC034 </pre> <br /> 그런데, 왠지 이상합니다. ecx, edx를 쓰는 걸로 봐서 __fastcall 규약으로 인자가 넘어가고 있는데 원래 x86에서의 DllImport의 기본 호출 규약은 __stdcall이기 때문입니다. 어쨌든 __fastcall 규약으로 보고, 처음 2개의 인자는 ecx, edx로 넘어가니 "SECURITY_ATTRIBUTES *" 인자는 [ebp-50h]에 해당합니다. Registers 윈도우를 통해 ebp == 0x0076ee00 임을 확인하고, 그럼 넘어가는 주소 값은 0x0076ee00 - 0x50 == 0x0076edb0이 됩니다. 이어서 메모리 창을 통해 0x0076edb0 주소의 값을 확인해 보니, 0xb8 0xed 0x76 0x00이 나옵니다. Little Endian이므로, 조립해 보면 0x0076edb8로 나오고 이 값은 pSec 변수의 주소와 일치합니다. (당연한 결과입니다.)<br /> <br /> 그래서 0x0076edb8 주소를 다시 메모리 창으로 보면, 다음과 같이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0c 00 00 00 80 53 b8 00 00 00 00 00 </pre> <br /> SECURITY_ATTRIBUTES 구조체로 대입해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > nLength == 0x0000000c lpSecurityDescriptor == 0x00b85380 bInheritHandle == 0x00000000 </pre> <br /> 와 같이 됩니다. 다시 lpSecurityDescriptor 주소(0x00b85380)의 값을 보면, 다음과 같이 20 바이트의 값이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 01 00 04 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 </pre> <br /> null dacl을 표현하는 것이므로 위의 값은 고정입니다.<br /> <br /> 이것으로 정상 동작할 때의 환경을 살펴봤습니다. 이제 다시 비정상적으로 동작했을 때의 코드로 분석해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static unsafe extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES lpSecurityAttributes</span>, bool isManualReset, bool initialState, string name); SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); // ...[생략]... SafeWaitHandle swh = CreateEvent(sa, true, false, "TEST_EVENT"); 02E101E4 lea eax,[ebp-48h] 02E101E7 push dword ptr [eax+8] 02E101EA push dword ptr [eax+4] 02E101ED push dword ptr [eax] 02E101EF push dword ptr ds:[42D2030h] 02E101F5 xor edx,edx 02E101F7 mov ecx,1 02E101FC call 0143C034 </pre> <br /> 역시 __fastcall 규약을 따르는 데 ecx 레지스터(즉, 첫 번째 인자)에 1이 넣어져서 전달되고 있습니다. 이는 위에서 예시했던 오류 메시지와 일치합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Exception thrown at 0x77900A4B (ntdll.dll) in NullDaclEvent.exe: 0xC0000005: <span style='color: blue; font-weight: bold'>Access violation reading location 0x00000001</span>. </pre> <br /> 재미있는 것은, SECURITY_ATTRIBUTES가 구조체이고 DllImport에 정의한 CreateEvent가 구조체로 첫 번째 인자를 받는다고 되어 있으므로 다음과 같이 SECURITY_ATTRIBUTES에 대한 구조체 복사가 이뤄지고 있다는 점입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 02E101E4 lea eax,[ebp-48h] 02E101E7 push dword ptr [eax+8] 02E101EA push dword ptr [eax+4] 02E101ED push dword ptr [eax] </pre> <br /> 그러니까, 인자 전달이 뒤죽박죽인 것입니다. "mov ecx, 1"은 CreateEvent의 2번째 인자인 true 값을 의미하고, "xor edx, edx"가 false 임을 감안할 때 sa 인자가 가장 마지막에 전달된 것입니다.<br /> <br /> 어쨌든, 왜 "Access violation" 오류가 발생했는지에 대한 답은 나왔습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그렇다면 x64 버전에서는 왜 잘 실행이 된 것인지 마저 짚고 넘어가겠습니다. 역시 CreateEvent 호출 부분을 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static unsafe extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES lpSecurityAttributes</span>, bool isManualReset, bool initialState, string name); SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); // ...[생략]... SafeWaitHandle swh = CreateEvent(sa, true, false, "TEST_EVENT"); 00007FFB4FED0356 mov r9,13CE3050h 00007FFB4FED0360 mov r9,qword ptr [r9] 00007FFB4FED0363 lea rcx,[rsp+38h] 00007FFB4FED0368 mov rax,qword ptr [rcx] 00007FFB4FED036B mov qword ptr [rsp+70h],rax 00007FFB4FED0370 mov rax,qword ptr [rcx+8] 00007FFB4FED0374 mov qword ptr [rsp+78h],rax 00007FFB4FED0379 mov rax,qword ptr [rcx+10h] 00007FFB4FED037D mov qword ptr [rsp+80h],rax 00007FFB4FED0385 xor r8d,r8d 00007FFB4FED0388 mov dl,1 00007FFB4FED038A lea rcx,[rsp+70h] 00007FFB4FED038F call 00007FFB4FD9C050 </pre> <br /> rcx에 [rsp + 70h] 위치의 주솟값이 전달되고 있습니다. 이때의 RSP 값이 0x0000000000DFE7B0이고, + 0x70 하여 구한 0000000000DFE820 주소를 lea로 로드했기 때문에 바로 그 주솟값을 메모리 창으로 확인해 보면 24바이트 짜리 SECURITY_ATTRIBUTES 구조체의 값이 이렇게 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 18 00 00 00 00 00 00 00 e0 5e fb 00 00 00 00 00 00 00 00 00 00 00 00 00 nLength == 0x0000000000000018 lpSecurityDescriptor == 0x0000000000fb5ee0 bInheritHandle == 0x0000000000000000 </pre> <br /> 당연히 0x0000000000fb5ee0 주소 값을 보면 이전의 x86 포인터 버전에서 확인했던 null dacl 값이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 01 00 04 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 </pre> <br /> x64 환경에서는 호출 규약이 처음 4개의 인자를 rcx, rdx, r8, r9에 전달하도록 되어 있습니다. rcx에는 SECURITY_ATTRIBUTES의 주소 값이 맞게 들어갔고, dl(즉, rdx)에는 1로 들어갔으니 true를 의미하므로 역시 정상입니다. 3번째인 false는 r8에 "xor r8d, r8d"로 되어 있으니 0 값이 잘 들어갔고, 4번째 인자인 "TEST_EVENT" 문자열 주소가 13CE3050h인데 "mov r9, 13CE3050h", "mov r9, qword ptr [r9]" 2개의 명령어로 잘 처리되었습니다.<br /> <br /> 재미있는 것은, SECURITY_ATTRIBUTES의 인자 전달 방식입니다. 원래는 [rsp + 38h] 주소에 SECURITY_ATTRIBUTES의 내용이 있음에도 불구하고 다음의 명령어로 [rsp+70h] 위치에 값을 그대로 복사하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 00007FFB4FED0363 lea rcx,[rsp+38h] 00007FFB4FED0368 mov rax,qword ptr [rcx] 00007FFB4FED036B mov qword ptr [rsp+70h],rax 00007FFB4FED0370 mov rax,qword ptr [rcx+8] 00007FFB4FED0374 mov qword ptr [rsp+78h],rax 00007FFB4FED0379 mov rax,qword ptr [rcx+10h] 00007FFB4FED037D mov qword ptr [rsp+80h],rax </pre> <br /> 따라서, 이런 경우에는 주소를 넘겼음에도 불구하고 Win32 API 내부에서 그 값을 변경한다 해도 호출자 측에는 반영되지 않습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 사실, 이것을 어떻게 해석해야 할지 모르겠습니다.<br /> <br /> 원래는 x64에서도 문제가 발생했어야 하는데 웬일인지 DllImport에 명시한 구조체로서의 SECURITY_ATTRIBUTES 인자를 넘기지 않고 주소 값으로 일부러 넘겨주는 처리를 하기 때문에 x64에서도 잘 동작한 것입니다. 반면, x86은 (정상적으로) 구조체로써 SECURITY_ATTRIBUTES 인자를 넘겼기 때문에 문제가 당연히 발생한 것이고!<br /> <br /> 따라서, 어쨌든 잘못된 코드이기 때문에 애당초 이 글에서 제시한 2가지 방식으로 인자를 넘기도록 처리하는 것이 바람직합니다.<br /> <br /> (<a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1094&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
5565
(왼쪽의 숫자를 입력해야 합니다.)