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로 디버깅을 해 봐야겠다.

2013년 11월 23일 토요일

Appler 디버깅하기 ___ 7

이미 수정까지 완료한 사항이지만, 지난 번과는 달리 이번에는 DOSBox를 수정해서 문제를 해결해 보았습니다.

지난 번에 수정을 했던 Ubuntu system의 HDD가 고장이 나서, 다른 HDD에 새로 시스템을 꾸미다 보니 결국은 같은 문제에 봉착하게 되었습니다.
그래서 이번에는 다른 방식으로 문제를 해결해 보고자 하는 생각에...


우선은 문제가 되는 그 화면
문제가 되는 화면, Path가 이상하고 Directory도 이상합니다.

이 문제는 DOS Interrupt 21h, AH=60h일 때의 오작동으로 인한 문제였음은 이미 앞선 포스팅에서 밝힌 바 입니다.

해당 부분을 처리하는 루틴은 dos.cpp의 DOS_21Handler()에 있습니다.
DOS_21Handler()

AH=60h (Canonicalize)가 해당하는 루틴입니다.

디버거를 이용해 trace를 해 본 결과 DOS_canonicalize()라는 함수를 수정하면 될 것으로 보였습니다.

DOS_files.cpp의 DOS_Canonicalize()


마지막에 Backslash만 붙여주면 되는 문제였기에, 문자열의 마지막에 Backslash가 있는지 없는지 검사해서 없을 때에만 붙여 주면 된다 생각했습니다.

그러나 이 루틴에 입력되는 문자열이 디렉토가 아닌 파일인 경우도 있기에, 무조건 Backslash를 넣어 주어서는 안됩니다.

몇번의 디버깅과 고민 끝에 입력 문자열의 마지막에 Backslash가 있다면 디렉토리이므로 이 경우에만 마지막에 Backslash를 검사하고 붙여주도록 수정하였습니다.

다음이 수정한 결과입니다.

위와 같이 수정하였습니다.

수정하고 빌드하여 확인한 결과 화면입니다.

이젠 정상 작동합니다.

2013년 11월 22일 금요일

vim/gvim에 대한 몇가지 의문

vim은 애초의 vi를 개량한 버전으로 알고 있습니다.
현재 널리 사용되는 vim의 홈페이지는 http://www.vim.org 입니다.

현재 Ubuntu 10.04 환경에서 사용하던 중에 이상한 부분을 발견하였습니다.
vim과 gvim이 하나의 실행파일이라는 점이고, 그럼에도 불구하고 ps에 보이는 결과는 또 다르다는 것이었습니다.

일단은 설치된 vim/gvim의 실행 파일이 하나라는 점은 다음에서 확인이 됩니다.
vim/gvim은 모두 /usr/bin/vim.gnome에 연결되어 있습니다.

gvim을 실행 한 후, "ps -aux"를 실행한 결과 화면
gvim을 실행한 결과, vim.gnome이 아닌 gvim으로 표시된다.


vim을 실행 한 후, "ps -aux"를 실행한 결과 화면
vim을 실행한 결과, vim.gnome이 아닌 vim으로 표시된다.

실행 파일 하나로 콘솔버전과 GUI버전을 통합한 것은 충분히 이해가 가능하지만, process 이름을 다르게 보이게 하는 신기합니다.

2013년 7월 20일 토요일

Appler 디버깅하기 ___ 6

마지막 포스팅에서 해결할 수 있는 방안은 제시했지만,
- 미해결 상태로 남겨둔 것이 찜찜하기도 했구요,
- 어셈블리 공부도 해야겠다 생각도 했구요,
- 또...Appler를 적극적으로 활용할 필요도 있고 해서 손을 대 보았습니다.

단지
1) 문자열의 마지막에 백슬래쉬가 있는지 여부를 검사하고
2) 없으면 백슬래쉬를 추가해주고
3) 있으면 그냥 놔둔다
는 단순한 루틴인데도 어셈블러로 짤 엄두가 잘 나지 않더군요.

그래서 비교적(?) 무대뽀 정신으로 해결해 보고자, 인터넷을 뒤졌습니다.
문자열 두개를 붙이는 strcat() 같은 어셈블리 소스코드를 찾으면 쉽게 변형해서 적용이 될 거 같았습니다.

http://assembly.happycodings.com/ : 별로 체계는 없지만 간단하고 유용한 샘플코드를 제공합니다.

http://cs.smith.edu/~thiebaut/ArtOfAssembly/artofasm.html : 뭔가 유명할 것 같은 이름이네요. The Art of Assembly Language Programming. PDF 도 존재하고 내용도 매우 충실해 보입니다. 컴퓨터의 구조에 대해서도 좋은 참고 자료가 되겠네요.

마침 두번째 사이트에서 strcat를 포함한 string 함수에 대한 부분이 있었습니다.
http://cs.smith.edu/~thiebaut/ArtOfAssembly/CH15/CH15-5.html#HEADING5-48

그런데 이건 해당 기능을 구현하는 것이 아니라 이용하는 겁니다.
UCR Standard Library라는 것이 존재하는데, 이 Library에 각종 string 함수들도 포함되어 있다고 하더군요.

해서 찾은 것이,
http://net.pku.edu.cn/~course/cs201/2003/mirrorWebster.cs.ucr.edu/Page_asm/ucrlib/stdlibv2.html
여기에 어셈블리 소스 코드도 모두 포함이 되어 있습니다.

strcat.asm이라는 파일이 있어서 찾아 보니,
strcat 프로시져

strcatx 호출하고 끝입니다.
그래서 strcatx 찾아 봤습니다.

strcatx 프로시져
알고 보면 별로 어렵지 않은데, 초짜인 저에겐 그저ㅠㅠ
그래도 어셈블리 명령어 찾아가며 가까스로 이해하고
이 부분을 변형해서 사용하기로 했습니다.
cld : Clear Direction
repne : Repeat while not equal
scasb : scan string(byte)
lodsb : load string(byte)
stosb : store string(byte)
대충 이런 의미들이더군요.
어셈블리 언어가 어려운 이유 가운데 하나가, 저런 명령어들이 묵시적으로 사용하는 레지스터/플래그 등을 기억해야 한다는 점인 거 같습니다.


이젠 DOSBox에서 Appler 소스를 고치겠습니다.
예전에 많이 보았던 DM.ASM의 ReadDirectory 부분을 다음과 같이 고쳤습니다.

DM.ASM

그리고 make
(사실은 이거 성공하기까지 한 5번은 고치고 디버깅하고를 반복했을 겁니다.)

make

Appler 실행하고 F3 눌러서 Disk Manager 실행하면 Path와 Directory 모두 잘 나옵니다.

Appler의 Disk Manager

허접하지만, 위에 소스를 보시면 무작정 마지막에 Backslash를 붙이는 게 아니라
마지막 문자가 Backslash가 아닐 때에만 붙이도록 고쳤습니다.

따라서 나중에 DOSBox가 문제점이 개선되어 나오더라도 Appler의 소스를 다시 건드리지 않아도 정상 작동할거라는 겁니다.^^


나이가 들어서 그런지, 저 짧은 루틴 분석하는 데에도 중간에 졸리고 귀찮고...그렇네요.
역시 프로그램은 만드는 것보다 분석하는 게 더 힘드네요.
그래서인지 다른 사람이 만든 소스를 분석해 보는 정성과 노력을 하는 사람은 틀림없이 성공할 수 있지 않을까 싶습니다.

이 일을 하면서 Appler를 수정했다는 자기 만족 보다는 UCR Standard Library라는 게 있다는 걸 배운 게 더 큰 성과가 아니었나 싶습니다.

2013년 7월 10일 수요일

Pentagram - Ultima8

Ultima의 여러 시리즈들이 remake 되고 있습니다.

이전에도 xu4라는 Ultima4의 리메이크 버전을 포스팅한 적이 있습니다.
http://re-coder.blogspot.com/2012/11/xu4-ultima4-recreated.html

우연히 이번에 접하게 된 것은 Pentagram이라는 것으로 Ultima8의 remake 버전입니다.

이런 프로그램들을 단순히 remake라고 부를 수는 없을 것 같은데,
딱히 뭐라고 불러야 할 지 모르겠습니다.

암튼... 이전의 xu4와는 달리 pentagram은 빌드에 별 문제도 없고, 실행에도 문제가 없었습니다. 포스팅을 해서 후일의 삽질을 방지할 필요는 없지만 그냥 화면만 보고 기분 전환이나 해 보겠습니다.


인트로

타이틀 Ultima8 Pagan


첫 장면

pentagram.ini를 수정하면 게임의 해상도, 배율, 화면의 크기 등을 조정할 수 있습니다.
위의 화면은 해상도 320x240, 배율 2, 화면 크기 640x480
다음의 화면은 해상도 640x480, 배율 1, 화면크기 640x480 입니다.

640x480 화면

홈페이지는 http://pentagram.sourceforge.net/

Appler 디버깅하기 ___ 5

앞 선 포스팅에서 대략의 결론을 내렸지만,
디버깅을 하는 또 다른 방법이 있기에 번외편으로 간략하게 소개하고자 합니다.

예전에 DOSBox의 내장 디버거에 대한 소개를 한 적이 있습니다.
http://re-coder.blogspot.kr/2013/06/dosbox.html

이 내장 디버거를 사용하여 디버깅을 하는 방법입니다.

먼저 내장 디버거를 사용하는 경우의 장점은,
- 소스가 없어도 됩니다.
- 소스가 있다고 해도 디버그 모드로 다시 빌드할 필요도 없습니다.
- 터보디버거와 같은 별도의 디버거가 필요 없습니다.

반면에 단점은,
- 소스가 있어도 소스레벨 디버깅이나 소스와 연계된 디버깅이 불가능 합니다.
- 내장 디버거의 제한된 기능을 이용해야 하기에 비효율적일 수 있습니다. (아무래도 전문 디버거보다야 기능과 편의성이 떨어집니다.)
- 어셈블리어/로우레벨 디버깅/리버스엔지니어링과 관련된 일정한 수준 이상이 되어야 디버깅이 가능합니다.

저의 경우엔 마지막 조건에 부합하지 못하는 관계로 엄두를 내지는 못 할 상황이지만, 이번 Appler의 경우에만 가까스로 디버깅이 가능했던 듯 합니다.

먼저 내장 디버거가 활성화된 DOSBox를 실행시킵니다.

DOSBox의 실행과 함께 표시된 디버거 화면


이제 디버깅을 하고자 하는 Appler를 실행시킵니다.

Appler 실행

DOSBox 화면에서 Alt-Pause를 누르면 실행이 중단되고 디버거에 입력이 가능하게 됩니다.
앞선 포스팅에서 알아낸 대로, 디버깅을 하기 위해 breakpoint로 지정할 부분이 INT 21 (AH=60h)입니다. 디버거에서 help를 입력하면 내장 디버거에서 사용할 수 있는 키와 명령어가 있습니다. 여기에 있는 대로 "bpint 21 60"을 입력 합니다.

INT 21 (AH=60h)에 대해 Breakpoint를 설정합니다.


"bplist"를 입력하면 breakpoint가 설정이 되었는지 확인이 가능합니다.
breakpoint가 설정되었음을 확인

이제 내장 디버거에서 F5를 눌러서 Appler가 계속 실행을 하도록 해 줍니다.


적당한 시점에 문제가 되는 화면으로 진입하도록 Appler에서 F3 키를 눌러 줍니다.

idle 상태에서 F3을 눌러 Disk Manager로 진입 시도

이제 바로 실행이 멈추고 디버거가 활성화 되었습니다.
실행이 멈춘 곳은 breakpoint로 설정한 int 21 입니다.

INT 21h에서 멈춘 상황

디버거에서 F10(Step Over)로 int 21을 수행 한 후, Alt-D를 눌러 DS:SI의 메모리 내용을 확인해 봅니다. 역시나 마지막에 Backslash가 없음을 알 수 있습니다.
INT 21h을 실행 후 메모리 확인

SM 명령어를 이용하여 마지막에 Backslash(5C)를 넣어 줍니다.
Set Memory로 backslash 추가

마지막에 Backslash가 추가되었음을 확인할 수 있습니다.
메모리 변경 확인

이제 다시 F5를 눌러 실행을 계속 하도록 합니다.
아마 2~3번 정도 같은 부분에서 멈출 것이고, 그 때마다 위의 과정을 반복해야 합니다.
(Step Over, Memory 확인, Memory 수정)

이렇게 하고 나면 정상적인 화면이 나오는 것을 볼 수 있습니다.
정상 화면

만약에 소스를 가진 DOS용 프로그램을 DOSBox의 내장 디버거로 디버깅을 하려면, 소스빌드시에 Map 파일과 Assembly Listing 파일을 만들도록 하면 디버깅에 조금 도움이 되지 않을까 싶습니다.