삽질하는플머

폼 디자이너 갖구 놀기

탐구생활/Delphi

델파이의 폼디자이너를 흉내내보았던 작업. 


무려 17년 전 골동품이라 생소한 코딩과  풋풋한 코멘트가 어색하네. 

마치 오래 전 일기장을 꺼내 읽는듯한 아스라한 느낌이 재미있다. 


델파이 5로 만들어졌으며 최신버전에서 빌드되는지 여부는 체크해보지 않았다. 





FormDesign.zip



김현수님이 델파이 최신버전(10.2 도쿄)에서 동작한다고 확인해주심. (캄솨~ 압도적 캄솨~~)









윈도 소켓의 프록시 DLL 만들기

탐구생활/Delphi

며칠 전 우연히 감동적인 강좌를 하나 읽게 되었다. 

 

http://www.codeproject.com/Articles/16541/Create-your-Proxy-DLLs-automatically 

 

양병규 형님이 "API Hooking 요점정리" 의 13번 항목에서 "단순 무식하고 쉽지만 실용성이 없다"고 소개한 그 기술에 무려 "실용성"을 부여하는 강좌 되시겠다. 

 

 

프록시 DLL을 만들 때 가장 어려운(귀찮은) 점은, DLL의 모든 함수에 대해 호출규약과 인자를 맞춰 원래 함수를 불러주는 일인데, 이 강좌에서는 이런 귀차니즘을 "__declspec(naked) " 라는 지시자를 써서 간단하게 해결하고 있다. 이 지시자는 VC++의 x86에서 지원되며, 호출시점의 스택상태를 그대로 유지하는 기능을 가진단다. (이번에 처음 구경함) 

 

델파이에서 이런 효과를 내려면 어떻게 하나 고민해봤는데, 스택 구조만 유지한다면 대충 이렇게 해도 잘 되는군. 

 

procedure Test();

asm

  jmp [원래주소]

end;

 

 

 

몰라도 되는 녀석들은 이런식으로 처리하고, 실제 가로챌 함수들에 대해서만 신경 쓸 수 있다는 얘기. 

 

 

그럼 이걸 어디에 쓰면 좋을까?? 

저 강좌와 마찬가지로 만만한 윈도 소켓 DLL을 가지고 놀아 보자. 

 

소켓 API를 사용하면 대충 wsock32.dll -> ws2_32.dll -> mswsock.dll 의 흐름으로 관련 라이브러리들이 링크된다. 

wsock32나 ws2_32 의 프록시를 어플리케이션과 동일한 디렉토리에 넣어두고 send() 나 recv() 같은 함수를 가로채면 패킷을 쉽게 감청할 수 있다.

 

 

델파이 코드를 뱉도록 강좌의 소스를 살짝 수정해 wsock32, ws2_32에 대한 프록시 DLL 프로젝트 파일을 만들어 보았다. 

 

Winsock_Proxy_Src.7z
다운로드

 

 

 

wsock32.dpr 의 주요부분은 다음과 같다. 

 

 

library wsock32;

 

uses

  Windows, SysUtils;

 
......
 
 

procedure __DLLProc(Reason: Integer);

begin

  case Reason of

    DLL_PROCESS_ATTACH:

    begin

      hl := LoadLibrary(PChar(GetSysDirectory + '\wsock32.dll'));

      if hl = 0 then Exit;

 

      OrgFuncs.Arr[0] :=  GetProcAddress(hl, 'accept');

      OrgFuncs.Arr[1] :=  GetProcAddress(hl, 'bind');

      ......

      OrgFuncs.Arr[73] :=  GetProcAddress(hl, 'AcceptEx');

......
 

// AcceptEx

procedure __E__73__();
asm
  jmp [OrgFuncs.Base + SIZE_OF_FUNC * 73]
end;
 

......

 

exports

  __E__73__ index 1141 name 'AcceptEx',

  __E__74__ index 1142 name 'GetAcceptExSockaddrs';

 

......

 

 

 

원본 DLL과 동일한 이름의 함수들을 Export 하고, 이 함수가 불릴 때 OrgFuncs 변수에 담아둔 원래 함수들의 주소로 점프시키고 있다. 호출시점의 스택구조를 유지하므로 원본 함수의 호출규약이나 인자값, 리턴값도 신경 쓸 필요가 없다. 

 

이제 실제로 AcceptEx 함수의 동작에 간섭해보자. WinSock 유니트를 열어 이 함수가 어떻게 정의되어있는지 배껴온다. 

 

function AcceptEx(sListenSocket, sAcceptSocket: TSocket;

  lpOutputBuffer: Pointer; dwReceiveDataLength, dwLocalAddressLength,

  dwRemoteAddressLength: DWORD; var lpdwBytesReceived: DWORD;

  lpOverlapped: POverlapped): BOOL; stdcall;

 
 
 

 __E__73__() 프로시저를 위 내용 그대로 재정의 해 보자. 

 

 

// AcceptEx

function __E__73__(sListenSocket, sAcceptSocket: IntPtr;

  lpOutputBuffer: Pointer; dwReceiveDataLength, dwLocalAddressLength,

  dwRemoteAddressLength: DWORD; var lpdwBytesReceived: DWORD;

  lpOverlapped: POverlapped): BOOL; stdcall;

type

  TAcceptEx = function (sListenSocket, sAcceptSocket: IntPtr;

    lpOutputBuffer: Pointer; dwReceiveDataLength, dwLocalAddressLength,

    dwRemoteAddressLength: DWORD; var lpdwBytesReceived: DWORD;

    lpOverlapped: POverlapped): BOOL; stdcall;

begin

  // 파라미터 가지고 노는 곳

  // ..........

 

  // 원래 함수 호출

  Result :=

    TAcceptEx(OrgFuncs.Arr[73])(

      sListenSocket, sAcceptSocket,

      lpOutputBuffer, dwReceiveDataLength, dwLocalAddressLength,

      dwRemoteAddressLength, lpdwBytesReceived, lpOverlapped

    );

end;

 

 

 

 

호출규약과 인자를 맞추어주고 다시 원래 함수에 그대로 전달하고 있다. TSocket 은 다시 선언하기 귀찮아 원래 타입인 IntPtr 형을 사용했다. 

리턴시 스택을 유지해야 하므로 원래 함수를 호출한 뒤 되도록 아무 일도 하지 말자. 지역변수 선언 또한 금물이니 주의하자. 

 

 


 

2016.06.28 추가. 

 

dumpbin 으로 얻어진 TXT 파일을 줏어담는것은 아무래도 모양이 빠져서... 

독립적으로 실행되는 간단한 유틸리티를 만들어 보았다. 이름하여 "프록시 DLL 생성기"

 

 

 

ProxyDllGen.7z
다운로드

 

 

실행시키고 DLL을 끌어다 던지면 Export 된 함수 목록을 보여준다. 

"Generate" 버튼을 누르면 해당 DLL의 델파이 버전 프록시 코드를 생성해준다. 

 

라자루스로 만들어봤는데, 못 본 사이에 참 좋아졌다. 터보델파이를 대체하기에는 조금 아쉽지만...

 

 


 

2017. 02.16

 

github 에 코드 올려둠. 

https://github.com/oranke/proxy-dll-generator

 

 

쓰레드에 이름 달아주기

탐구생활/Delphi

쓰레드 코딩을 하다보면... 디버깅창에 줄줄이 나타난 쓰레드가 어디서 만든 뭐하는 녀석인지 궁금할 때가 있다. 

이 때 각 쓰레드마다 이름을 붙여둔다면 심적으로나마 편안함을 얻을 수 있지 않을까... 


물론 디버깅에서만 유효하지만, 좋은 내용이 있어 공유한다. 


http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx

http://blogs.msdn.com/b/stevejs/archive/2005/12/19/505815.aspx


const

  MS_VC_EXCEPTION = $406D1388;

  

type

  TThreadNameInfo = record

    dwType: DWORD;      // Must be 0x1000.

    szName: PAnsiChar;  // Pointer to name (in user addr space).

    dwThreadID: DWORD;  // Thread ID (-1=caller thread).

    dwFlags: DWORD;     // Reserved for future use, must be zero.

  end;


procedure SetThreadName(aThreadID: DWORD; const aThreadName: PAnsiChar);

var

  tmi: TThreadNameInfo;

begin

  if (DebugHook = 0) then Exit; 


  tmi.dwType := $1000;

  tmi.szName := aThreadName;

  tmi.dwThreadID := aThreadID;

  tmi.dwFlags := 0;

  try

    RaiseException(MS_VC_EXCEPTION, 0, sizeof(tmi) div sizeof(LongWord), PDWORD(@tmi));

  except

  end;

end;



사용법은 다음과 같이. 


procedure TForm1.FormCreate(Sender: TObject);
begin
  SetThreadName(GetCurrentThreadID, 'Main thread'); 
end;


IDE의 디버깅창을 들여다보면, 예쁘게 이름이 붙은 쓰레드를 확인할 수 있다 . 



키보드 타입 3 에서 컨트롤+스페이스 사용하는 법

탐구생활/Delphi



맥북에어를 데려왔다. 가볍고 빠르다. 좋다. 

윈도를 올려봤다. 응? 한영키가 없네? 어차피 키보드 타입 3를 쓰니까 패스!

터보델파이를 깔았다. 컨트롤+스페이스가 안먹는다. 우측 컨트롤키가 없으니 좌컨트롤+우컨트롤+스페이스 신공도 안통한다. 

영문 입력기를 추가하고 알트+쉬프트를 며칠 써보니... 익숙치 않은 키입력 때문에 손가락이 꼬이는 느낌이다. 


타입 3 키보드에서 컨트롤+스페이스는 한자변환키이다. 

가만히 생각해보니 IME모드가 영문일 때 이 키는 별로 필요가 없다. 

그렇다면? 입력 포커스를 가진 윈도가 한글입력 상태가 아닐 때 이 한자변환키를 무력화하고

좌컨트롤+우컨트롤+스페이스 키스트로크를 조립해주면 되겠군. 


그래서 탄생한 물건이 이 놈. 



KbdT3Help.7z




한글 IME 에서 영문모드일 때 컨트롤+스페이스 입력을 좌컨트롤+우컨트롤+스페이스 로 변환해준다. 

한 번 실행시키면 동작하고 다시 실행시키면 정지한다. 






이제 좀 살만하네~~


------------



터보델파이로 만든 32비트 어플이지만, DLL을 침투시키지 않기 때문에 64비트 프로세스에서도 잘 돌아간다. 

64비트 윈도7에 띄운 64비트 라자루스에서도 동작함. 

DLL 만들기 귀찮아서 게으름을 피웠는데... 의도하지 않은 "사이드 이펙트"... 훗~!

귀차니즘은 역시 프로그래머의 가장 큰 미덕이다. 


관리자모드로 실행시킨 어플에서도 동작하게 하려면, 이 유틸리티도 관리자모드로 띄울 것. 







2014.3.14. 

PHPer 님의 의견대로, 아무 파라미터나 지정되면 메시지박스를 띄우지 않도록 수정. 

새로 받으세요~~ 





2017.02.16

차일피일 미루다 이제야 github에 등록함. 

https://github.com/oranke/kbd-type3-helper


코헨-서더랜드의 알고리즘 이야기

탐구생활/Delphi
어지간한 컴퓨터 그래픽 이론책자에 빠짐없이 등장하는 이 알고리즘은 1967년도에 만들어진 대표적인 클리핑 알고리즘이다.  
컴퓨터라는 물건 자체가 일반적이지 않던 시절, 직선을 뷰포트에 맞추어 잘라주기 위해 이런 고민을 했다는 사실이 그저 놀랍기만 하다. 

졸업연구 과제를 선택할 때, 밑천이 필요없는 과제를 고르다보니 컴퓨터 프로그램을 만들게 되었고 
배운게 도둑질이라고 설계->가공 과정에서 뭔가 건드려볼만한 주제라고 선택한 것이
오토캐드의 DXF 파일을 가공용 NC 코드로 바꿔보자는 시도였다. 

당시까지 짜본거라고는 오토캐드의 리습과 배치파일밖에 없던 무식한 시절,
일년 후배 여모씨를 족쳐가며 익숙치않은 터보C로 겉모습만 그럴듯한 DXF뷰어를 만들어가는데... 
아... 이거 이상하다. 줌인/줌아웃 기능을 넣고 선을 확대하다보니 엉뚱한 곳에 찌꺼기 선이 죽죽 그려진다. 
지금 생각하면 뷰포트를 한참 벗어나는 선을 그릴 때 사용한 변수값이 오버플로해서 생겨난 현상일텐데,
인터넷이 없던 시절이라 어디 물어볼 곳도 마땅치 않고 그저 냉가슴만 앓을 뿐...

그러다 눈에 띈 책이 지금도 보물처럼 간직하고 있는 이 책. 
꾸벅꾸벅 졸면서 들었던 컴퓨터 그래픽스 개론이라는 수업의 교재. 



수업시간에는 그저 지루해 죽겠더니만, 막상 상황이 닥치니 어쩌면 그렇게 머리에 쏙쏙 들어오는 옳은말만 적혀있는지...
뷰포트 클리핑을 적용한 뒤 마음껏 확대해도 더 이상 쓰레기 정보가 나타나지 않는다는 사실이 어찌나 신기하던지... 
그리하여 이 알고리즘은 책의 내용을 실제 코드로 만들어 본 최초의 경험이 되었다.


뭐 지금은 세월이 하도 좋아져서... 어지간한 확대기능 정도로는 당시 경험했던 오버플로는 구경할 수도 없다.
게다가 3D API를 쓰면 삼각형의 클리핑도 하드웨어에서 구현되버리니 이 알고리즘은 예전 추억과 함께 봉인!!
살면서 다시 써 볼 일이 있을까 싶었는데... 



(그런데 그 일이 실제로 일어났습니다!!!)



먹고 사는 이야기니 자세한 내용은 말할 수 없고,

좀 많이 넓은 높이맵과 이 위를 지나가는 물체와의 충돌점을 계산해야 하는 상황에 처했다. 
물체가 지나가는 경로상의 셀들을 추출하는 문제는 브랜슨햄, 또는 GPG의 네비메시 코드를 써먹으면 되는데
문제는 물체의 이동경로가 아주 길거나 높이맵과 전혀 상관없는 곳을 지나갈 때, 
검사범위를 높이맵 이내로 제한하거나 아예 검사를 생략하려면 어떻게 해햐 할까. 

자연스럽게 이 캐캐묵은 알고리즘이 떠올랐다.
선분과 뷰포트의 관계에 따라 아예 그리지 않거나 뷰포트 이내로 잘라 그리게 하는 절묘한 비법. 
이 비법을 쓰면 물체의 긴 이동경로 중 높이맵과 관계된 부분만 잘라내거나 
이동경로가 높이맵과 관계없다면 버릴 수도 있게 된다. 

위키백과를 열어보자. 
http://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland 

친절한 C코드도 제공된다. 아이조아~ 
이 코드는 우리가 산수시간에 배웠던 1사분면 좌표계를 기준으로 하므로 좌하단이 원점이다. 
내가 사용하는 좌표계는 무조건 좌상단이 원점이므로 이 부분만 주의해 델파이로 옮겨보자. 

function CohenSutherlandLineClip(var x1, y1, x2, y2: Single; const xmin, ymin, xmax, ymax: Single): Boolean;
const
  V_INSIDE = 0;
  V_LEFT   = 1;
  V_RIGHT  = 2;
  V_BOTTOM = 4;
  V_TOP    = 8;

  function ComputeOutcode(const x, y: Single): LongWord;
  begin
    Result := V_INSIDE;
    if x < xmin then Result := Result or V_LEFT else
    if x > xmax then Result := Result or V_RIGHT;

    if y < ymin then Result := Result or V_TOP else
    if y > ymax then Result := Result or V_BOTTOM;
  end;

var
  OutCode1, OutCode2: LongWord;
  ChkCode: LongWord;
  x, y : Single;
begin
  Result := false;

  OutCode1 := ComputeOutcode(x1, y1);
  OutCode2 := ComputeOutcode(x2, y2);

  while true do
  begin
    // Bitwise OR is 0. Trivially accept and get out of loop
    if not Boolean(OutCode1 or OutCode2) then
    begin
      Result := true;
      Break;
    end else
    // Bitwise AND is not 0. Trivially reject and get out of loop
    if Boolean(OutCode1 and OutCode2) then
    begin
      Break;
    end else
    begin
      // At least one endpoint is outside the clip rectangle; pick it.
      if Boolean(OutCode1) then
      begin
        ChkCode := OutCode1;
        x := x1;
        y := y1;
      end else
      begin
        ChkCode := OutCode2;
        x := x2;
        y := y2;
      end;
      // Now find the intersection point;
      // use formulas y = y0 + slope * (x - x0), x = x0 + (1 / slope) * (y - y0)
      if Boolean(ChkCode and V_TOP) then
      begin
        x := x1 + (x2 - x1) * (ymin - y1) / (y2 - y1);
        y := ymin;
      end else
      if Boolean(ChkCode and V_BOTTOM) then
      begin
        x := x1 + (x2 - x1) * (ymax - y1) / (y2 - y1);
        y := ymax;
      end else
      if Boolean(ChkCode and V_RIGHT) then
      begin
        y := y1 + (y2 - y1) * (xmax - x1) / (x2 - x1);
        x := xmax;
      end else
      if Boolean(ChkCode and V_LEFT) then
      begin
        y := y1 + (y2 - y1) * (xmin - x1) / (x2 - x1);
        x := xmin;
      end;

      // Now we move outside point to intersection point to clip
      // and get ready for next pass.
      if ChkCode = OutCode1 then
      begin
        x1 := x;
        y1 := y;
        OutCode1 := ComputeOutCode(x1, y1);
      end else
      begin
        x2 := x;
        y2 := y;
        OutCode2 := ComputeOutCode(x2, y2);
      end;
    end;
  end;
end;



테스트는 대충 다음과 같이. 
뷰포트는 윈도 클라이언트 영역에서 80픽셀씩 안쪽으로 잡았다. 
마우스를 두 번 눌러 입력받은 선분을 그려주고 클리핑을 통과한 선은 빨간색으로 조금 굵게 그려주자. 
전역 변수 따로잡기 귀찮으니 J+ 지시자를 써서 상수를 static으로 처리. 

......

{$J+}

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
const
  CLICK_COUNT : Integer = 0;
  LAST_POS: TPoint = (x:-100; y:-100);
var
  x1, x2, y1, y2: Single;
  xmin, ymin, xmax, ymax: Single;
begin
  case CLICK_COUNT of
    0: LAST_POS := Point(X, Y);
    1:
    begin
      Canvas.Pen.Width := 1;
      Canvas.Pen.Color := clBlack;
      Canvas.MoveTo(LAST_POS.X, LAST_POS.Y);
      Canvas.LineTo(X, Y);

      xmin := 80;
      ymin := 80;
      xmax := ClientWidth - 80;
      ymax := ClientHeight - 80;

      x1 := LAST_POS.X;
      y1 := LAST_POS.Y;
      x2 := X;
      y2 := Y;

      if CohenSutherlandLineClip(x1, y1, x2, y2, xmin, ymin, xmax, ymax) then
      begin
        Canvas.Pen.Width := 2;
        Canvas.Pen.Color := clRed;

        Canvas.MoveTo(Round(x1), Round(y1));
        Canvas.LineTo(Round(x2), Round(y2));
      end;
    end;
  end;

  Inc(CLICK_COUNT);
  CLICK_COUNT := CLICK_COUNT mod 2;
end;

procedure TForm1.FormPaint(Sender: TObject);
begin
  Canvas.Brush.Style := bsClear;
  Canvas.Pen.Color := clBlue;
  Canvas.Pen.Width := 1;
  Canvas.Rectangle(80, 80, ClientWidth-80, ClientHeight-80);
end;

...... 



결과는 다음과 같다. 파란 뷰포트 안에 포함되는 직선만 굵고 붉게!! 그려진다.  

 
 
아이~ 아름다워라~

방문자를 위한 마지막 조공짤은 위키백과에서 찾은 이반 서더랜드옹의 사진 

  


그래. 대머리와 흰머리도 그 속에 채워진게 많으면 저렇게 간지가 좔좔 넘치잖아??!!
마누라가 아무리 압박해도 염색따위는 하지 말자!!! (귀찮아서가 아님!!!)