2013년 12월 22일 일요일

Appler 디버깅하기 ___ 버그 발생

Appler 디버깅하기
라는 제목으로 게시했던 1번부터 6번까지의 게시물을 무효화 하겠음.

무효화 이유는,
수정된 Appler의 작동에 문제가 발생했기 때문.
Apple 게임인 2400AD를 실행하면, 초기 로고화면이 지나고 게임 화면의 프레임만 그려진 상태에서 Appler가 Halt 됨.

정상적으로 시작된 게임 화면

수정된 Appler는 이 화면에서 멈추어 버림

7번 게시물에서 제시한 방법대로 DOSBox를 수정한 버전에서는, 원래의 Appler로 문제없이 진행이 됨을 확인하였음.
하지만 1~6 게시물의 방법으로 수정된 Appler는 어떤 DOSBox에서도 문제가 있었기에, 수정된 Appler의 문제임은 확실함.


추정 원인은,
마지막 6번 게시물에서 만든 수정코드가 아니라,
1번과 2번 게시물에서 소스를 빌드하는 과정에 컴파일 오류를 수정하는 과정에서 발생한 것으로 생각 됨.
왜냐하면, 6번게시물에서 수정한 부분은 단지 Disk Manager 화면에만 영향을 줄 뿐 임.


확인 해 본 사항들
  • 1번과 2번 게시물에서 수정했던 부분에 대한 이해가 부족했기에, 이 부분을 제대로 고치기 위해서 어떻게 작동하는지 확인해 보아야겠다고 생각했음.
  • 이를 위해서, 수정한 부분이 실행파일에 어떤 식으로 컴파일되어 들어가 있는지 확인해야 하며, 원래의 실행파일에는 어떻게 컴파일되어 들어가 있는지 확인을 해야 했음.
  • 일단 수정한 실행 파일과 원래의 실행 파일을 비교하니 전자는 .EXE 파일이었고, 후자는 .COM 파일이었음.
  • 더욱이 시작 코드는 더욱 달라서 전자는 xor al, al이었고 후자는 mov ax, 4xxxH 였음.
  • 아무래도 바로 비교는 힘들었기에 EXE를 COM으로 바꿔보기 위해서 EXE2BIN을 사용해 보았음.
  • FreeDOS용 EXE2BIN은 모두 실패했음
  • MS-DOS 6.22용 EXE2BIN은 DOSBox에서 버전 불일치로 실행이 안 됨. DOSBox의 MS-DOS 버전은 5.0으로 되어 있음.
  • MS-DOS 5.0용 EXE2BIN은 Insufficient Memory라는 에러를 내며 실패했음.
  • Sourcer를 사용해 보았지만, 앞서와 비슷하게 (xor al,al, mov ax,...)차이가 나기에 모든 소스를 다 이해하기 전에는 원하는 부분이 어떻게 컴파일되어 들어가 있는지 찾아내기는 거의 불가능해 보임.
  • 심지어는 과연 Appler의 소스가 실행파일의 소스가 맞는지 의심되기도 함.
  • Appler의 소스는 Zophar's Domain에서만 받을 수 있었고, Appler의 공식 사이는 없기에 신뢰성이 떨어짐.
  • Appler의 소스에 있는 문서로는 터보어셈블러 2.x와 4.x로 빌드 되는 것을 확인했다고 했는데, 나는 터보 어셈블러 5.0으로 빌드하였던 것이기에 터보 어셈블러 2.0을 구해서 빌드해 보았으나 더 많은 에러가 발생함. 이 때문에 소스에 대한 신뢰성이 더 떨어지게 되었음.

이상의 실험 결과, 주어진 소스를 활용한다는 것 자체에 의문이 들어,
DOSBox를 수정하는 방법인 7번 게시물만을 유효한 것으로 인정하고
1번 ~ 6번 게시물은 무효화 하기로 결정했음.

2013년 12월 19일 목요일

DOSBox의 커맨드 쉘(Command Shell) 버그 ___ 2

먼저 간단하게 정리한 GDB의 명령어들.




여기에 정리한 명령어 외에,
(gdb) until <line>
: <line>까지 실행
(gdb) return
: 현재 함수 벗어나기

이 정도의 명령어만 알고 있어도 디버깅이 가능했다.
(이번 디버깅에선 register, memory dump, call stack에 대한 접근이 불필요했기에...)
단, GDB에서의 source listing 기능은 아무래도 많이 부족하므로 별도의 소스 뷰어와 사용하면 좋을 듯 하다.


이제, 본격적으로 디버깅!

DOSBox를 타깃으로 gdb를 실행하고 바로 run.

DOSBox가 시작된 후에, 문제를 확인하기 위해 D drive로 바꾸어 둔다.

CTRL-C를 눌러서 잠시 중단시킨다.
break로 DOS_Shell::Execute에 브레이크포인트를 설정하고, c로 다시 실행을 재개한다.

문제의 "nc"를 입력한다.

앞선 포스팅에서 문제가 되는 부분의 함수 호출 순서를 정리했다.
다시 보면,

shell/shell.cpp 286 DOS_Shell::Run()
shell/shell.cpp 295 temp.ParseLine()
shell/shell.cpp 199 DOS_Shell::ParseLine()
shell/shell.cpp 251 DoCommand()
shell/shell_cmds.cpp 118 DOS_Shell::DoCommand()
shell/shell_cmd.cpp 153 if(Execute())
shell/shell_misc.cpp 358 DOS_Shell::Execute()
shell/shell_misc.cpp 387 Which()
shell/shell_misc.cpp 511 DOS_Shell::Which()
shell/shell_misc.cpp 572 if(DOS_FileExists())...

이 가운데 DOS_Shell::Execute()에 브레이크포인트를 설정해 둔 상태였고,
"nc"를 입력하여 이 브레이크포인트에서 멈추기를 기대하였다.


DOS_Shell::Execute에서 멈추었고, 이후의 소스를 확인한다.

중간과정은 관심밖이므로, 원하는 387 line Which()까지 실행하도록 u 387을 입력한다.

Which() 내부의 처리를 보기 위해 s(tep)을 입력하고 Which의 처리과정을 확인한다.
현재 위치(D:\)에서 확장자(.com, .exe, .bat)를 붙여가며 파일이 존재하는지 확인한다.
당연히 없으므로 Which() 함수에서의 처리가 계속된다.

이제는 PATH를 하나씩 순서대로 뒤져가며 파일을 찾는다.
중간에 확인하고 싶은 변수는 p(rint) 명령을 사용한다.

PATH 환경변수에서 하나의 경로만을 떼어내는 과정이었다.

첫번째 PATH인 Z:\에 대해서 입력된 nc를 붙여 다시 파일을 찾는다.

다음인 C:\FREEDOS도 실패하고,
그 다음인 C:\에 대해서는 "u 571"이 이전과는 달리 571 line에서 멈추지 않았다.

C:\NC가 아닌 C:\에서 "nc"라는 파일을 찾았다고 판단하게 된 것이 문제의 원인이었다.

Which() 함수는 대체로 다음의 순서대로 파일을 찾는다.

1) 현재 위치에서 "nc"
2) 현재 위치에서 "nc.COM"
3) 현재 위치에서 "nc.EXE"
4) 현재 위치에서 "nc.BAT"
5) PATH에 있는 각 경로에 대해서 위의 1) ~ 4)를 반복 검색

문제가 된 것은 "C:\" 경로에서 "nc"를 찾는 것이 성공했기 때문이다.
어째서 일까?
DOS_FileExists()는 다음과 같은 순서로 진행이 된다.

dos/dos_files.cpp 1176 DOS_FileExists()
dos/dos_files.cpp 1179 return Drives[drive]->FileExists(fullname);
dos/drive_local.cpp 374 localDrive::FileExists()
dos/drive_local.cpp 380 FILE* Temp=fopen(newname,"rb");

최종적으로 불리는 fopen() 함수의 인자로 주어진 경로는 파일이 아닌 폴더였지만, 폴더에 대해서도 fopen()이 성공한 것으로 처리했던 것이다.

nc와 pe2만이 문제가 되었던 것은, 서브디렉토리의 이름과 실행파일의 이름이 동일했기 때문이다.

자, 이제 문제의 원인이 밝혀졌으니 이를 해결할 방안은 세워야겠다.
1) Path와 실행파일의 이름이 같지 않도록 만든다.
2) 가장 depth가 큰 path가 앞에 오도록 path의 순서를 바꾼다. ( C:\;C:\NC -> C:\NC;C:\ )
3) Source 수정 : File인지 디렉토리인지 구분하여, 파일인 경우에만 존재여부를 확인 할 수 있도록 한다.
4) Source 수정 : File의 존재 여부를 확인할 때, 확장자가 없는 경우에는 확장자를 먼저 붙인 다음에 존재여부를 확인하도록 한다.

1)과 2)의 방법은 실제로 효과가 있음을 확인하였다.
하지만 3)과 4)의 방법은 시도도 하지 못했다.

디버깅을 하면서 느낀 점은, Which() 혹은 ParseLine()이라는 부분의 처리가 매우 "땜질"식으로 짜여져 있는 느낌을 받았기 때문이다.
비효율적인 부분(PATH를 매번 분리하는 작업), 중복처리(확장자를 붙여서 찾는 부분이 중복되어 "nc.COM.COM"을 찾기도 한다.) 등의 문제가 있는 것으로 미루어보아, 여러 문제가 발생해서 수정이 여러번 이루어졌으며 그나마도 한사람이 아닌 여러 사람이 고친 것으로 보였기 때문이다.
만약 내가 또 하나의 수정을 가하면, 점점 걸레화 되어갈 것이 뻔하다.
그리고 내가 모른 문제가 다시 튀어나올지도...

따라서 1)과 2)의 방법으로 문제를 해결하길 권한다.


마지막으로 이 문제가 어째서 M$-Window$에서는 발생하지 않는지 확인해 보았다.

직접 디버깅해 본 결과 M$-Window$에서는 fopen()의 인자로 폴더가 주어졌을 때, 실패한 것으로 처리가 됨을 확인했다.

2013년 12월 15일 일요일

DOSBox의 커맨드 쉘(Command Shell) 버그 ___ 1

DOSBox 0.74

Ubuntu Linux 10.04

Norton Commander를 PATH에 설정하였으나, 종종 nc.exe를 찾아서 실행하지 못하는 문제점 발생.

DOSBox의 내장 명령어인 imgmount를 실행 한 후,
Turbo Assembler로 프로젝트를 빌드한 후에 증상 발생.

프롬프트에서 "nc"를 입력하면
"Illegal command: nc." 이라는 오류를 내뱉는다.

환경변수인 PATH는 아무런 문제없이 정상으로 보인다.
프롬프트에서 "set"을 입력하면
"
COMSPEC=Z:\COMMAND.COM
BLASTER=A220 I7 D1 H5 T6
PATH=Z:\;C:\FREEDOS;C:\;C:\NC;C:\PE2;C:\TASM5\BIN
"

위의 PATH에서 문제가 되는 부분은 C:\NC와 C:\PE2 두 군데 뿐이다.
"nc"와 "pe2"를 입력하면 실행되지 않고 있는 것이다.

다음의 화면을 보면 문제점의 상황을 이해하기 쉬우리라 생각한다.

Autoexec.bat까지 실행된 초기 화면

D:\에서는 nc와 pe2 모두 실행되지 않는다.


왜 그럴까?

여러가지 실험 결과 "nc.exe", "pe2.exe"를 입력하면 문제없이 실행이 된다.

또 한가지, C:\에서는 정상적으로 실행이 된다.

조금 더 실험을 해 본 결과, 어떠한 작업도 하지 않았어도 이 문제는 발생하고 있으며,
C:\를 제외한 다른 곳에서는 항상 문제가 발생하는 것으로 보인다.

C:\에서 nc를 실행하면,

Norton Commander가 정상적으로 실행된다.

C:\에서 pe2를 실행하면,

Personal Editor2가 정상적으로 실행된다.



디버깅을 하기 위해 DOSBox의 소스를 검토해 보았다.

일단은 src/shell/shell_cmds.cpp의 DOS_Shell::DoCommand()가 가장 유력해 보인다.
이 함수의 호출 경로를 역추적 해 보니,

src/shell/shell.cpp의 DOS_Shell::ParseLine()

src/shell/shell.cpp의 DOS_Shell::Run()

함수의 호출 순서대로 경로를 정래하면 다음과 같다.


shell/shell.cpp 286 DOS_Shell::Run()
shell/shell.cpp 295 temp.ParseLine()
shell/shell.cpp 199 DOS_Shell::ParseLine()
shell/shell.cpp 251 DoCommand()
shell/shell_cmds.cpp 118 DOS_Shell::DoCommand()
shell/shell_cmd.cpp 153 if(Execute())
shell/shell_misc.cpp 358 DOS_Shell::Execute()
shell/shell_misc.cpp 387 Which()
shell/shell_misc.cpp 511 DOS_Shell::Which()
shell/shell_misc.cpp 572 if(DOS_FileExists())...


Ubuntu에서 사용할 수 있는 디버거는 GDB가 대표적이지만,
실행과 동시에 깝깝해지는 "(gdb) " 프롬프트만 껌벅이는 화면을 보게 된다.
메뉴 방식이 아닌 명령어 입력 방식인 GDB의 사용을 편리하게 하기 위해
DDD(Data Display Debugger)라는 GDB의 Frontend 프로그램이 있기도 하지만,
실제 사용해 본 결과, 메모리 사용량이나 느린 응답 속도 때문에 사용이 꺼려진다.

이번 기회에 GDB의 명령어를 배워서 직접 GDB로 디버깅을 해 봐야겠다.