온갖 상상이 난무하고, 추측과 확신이 교차하는 작업이 디버깅이라고해도 과언이 아니다. 디버깅은 아는 것 만큼 혹은 조금 더 상상한 것 만큼만 해결 가능하고, 그 외의 것들은 모두 우연한 실수일 뿐이다. 어쩌다 문제를 해결했어도 그것은 실수로 해결한 것이리라. 잔인한가?

디버깅이야말로 책으로 보아왔던 지식이 살아나는 현장이고, 디버깅이야말로 책을 들여다보게 만드는 작업이다. 디버깅을 하면서 가장 중요한 자세 하나를 생각해보고자 한다.

디버깅의 가장 큰 적은 "그 부분은 문제없을텐데"라고 믿게되는 근거를 알 수 없는 자기확신이다. 만일 디버깅을 잘하고자한다면, 지금까지 확실하다고 생각했던 부분을 다시 한 번 보라.

프로그래머가 가져야할 가장 중요한 덕목 중에 하나는 논리적인 무결성과 실제 데이터의 무결성에 대한 차이를 알고 그것을 상황에 따라 적절히 펼치는 것이다. 누구나 프로그래머라면 프로그램의 무결성을 추구한다. 즉, 오류에 적절히 처리하는 루틴이 삽입된다. 그런데, 가끔 명령한 동작의 결과가 성공일 것이라는 맹신을 할 때가 있고, 그것은 논리적으로 아무 오류가 없을 것이라는 생각으로 슬쩍 넘어가게 된다. 그러나 실행시간에 실제 데이터가 그 동작을 보장할 수 없는 값으로 넘어 왔고, 여기에는 논리적으로 있을 수 없다는 것에서 오류처리를 하지 않았고, 이것 때문에 버그가 생기는 것이다.

물론 디버깅을 획일화할 생각은 없지만, 가장 도움이 되는 버그 퇴치자세를 논하기 위해 다른 상황은 잠시 무시하자. 그렇다면, 논리적 무결성과 실데이터의 무결성에 대한 차이는 무엇일까?

논리적으로 무결한 예는 이렇다.
정수를 입력받고, 이 값을 2로 나누면 짝수 아니면 홀수임을 알 수 있다.

하지만 다음은 논리적으로 무결하지 않다.
내가 지금 300바이트를 TCP/IP를 통해서 한번에 전송하였다. 그러면 수신쪽에서도 한 번 읽을 때 300바이트가 읽혀질것이다.

다음은 어떠한가?
두 개의 쓰레드가 돌아가는데, 한 쓰레드에서 스택에 생성된 자동변수 두개의 값을 바꾸기 위해 하나를 더 잡아서 임시 보관용으로 사용하면 두 개의 값을 바꿀 수 있다
스택은 쓰레드마다 고유한 것이고, 따라서 위 말은 아무런 문제가 없다. 논리적으로 아무런 문제가 없으니 이런 루틴은 설마 버그가 있었으랴하고 지나치는 것이 우리의 상식이다. 하지만, 좀더 염세적으로 생각해보자, 쓰레드는 다른 쓰레드의 스택에 접근할 수 있는 권한이 있다. 혹시 이전 작업이 같은 함수내의 자동변수에 대한 포인터가 다른 쓰레드에 넘어 갔고 그쪽에서 오버플로라도 일어났다면? 심각한 문제이다.

인생은 디버깅이다. 적절히 assert 뿌려가면서 사는 것이지.
요즘에야 프로젝트의 시작부분에 설치본을 만드는 것이 당연하다 생각되나, 나 자신도 몇년전에는 그러하다 생각지 못하였는데, 그 이유는 "설치할 것이 있어야 설치본을 만들지!"라는 생각에서였다.

하지만, 제일 쉬우면서도 가장 오래, 배포되는 프로그램의 끝까지 애를 먹이는 것이 바로 배포를 위한 설치본, 또는 부분패치 설치본이며, 이것은 릴리즈 엔지니어링의 마지막 결과물에 해당한다.

강조를 백번해도 모자랄 정도로 프로젝트의 시작부분에서의 설치본인데, 이것은 개발조직이 분화되기 위한 첫걸음이된다. 개발초기부터 테스팅 및 릴리즈를 위한 얘기를 할 수 있고, 중간 단계쯤에서 그간 진행된 기능에 대한 다양한 피드백을 받을 수도 있고, 나중에 고생할만한 일을 초기에 잡을 수 있는 아주 필수적인 것이 개발팀이 설치본을 만들어 배포하는 것이다. 그것이 비록 불완전하게 동작한다할 지라도, 설치본이 대화의 중심에 있어야한다.

다양한 계층의 테스터, 성능이나 UI에 대한 피드백을 프로그래머들이 오너십을 가지고 진행한다는 것은 얼마나 피곤한 일인가? 그것은 전문성도 떨어지려니와 프로그래머에게 설계/구현/디버깅이라는 행위에서 의견 수집/분석/조율이라는 행위간의 문맥전환을 수시로 일으키기란 참으로 어려운 일이다.

설치본을 어떻게 만들것이냐는 것은, 초기에는 대~~충 설치본을 만드는 한이 있더라도 초안을 생각해두고 진행해야한다. 설치 및 삭제 그리고 부분 업그레이드에 대한 것이 나중에 버그를 잡는데도 도움이 된다. 그 이유는 한 번 개발팀을 떠난 것은 어떤 식으로든, 부메랑을 타고 돌아오게 되어 있다. 그런데, 문제는 개발팀의 손을 한두번 떠난것이 아닌 상황이 오게되면, 부메랑에 맞아 쓰러지는 상황이 발생하게 된다.

몇가지 정리하자면, 설치본을 만들때는 다음과 같은 상황을 고려해야한다.

1. 매일매일 최신소스로 설치본이 자동으로 만들어지는가?
2. 임의의 바이너리가 어느 빌드에서 생성되었는지 알 수 있는가?
3. 바이너리들이 의존하고 있는 라이브러리들은 모두 알고 있는가?
4. 바이너리안에 들어 있는 global symbol(nm 명령을 통해)들은 관리가 되고 있는가?
5. 이전 설치본과 지금 설치본에서 변경사항은 자동으로 뽑혀 릴리즈 노트를 구성할 수 있는가?
6. 부분 테스트를 위한 정보들은 버그트래킹시스템과 연결되어 꼭 필요한 인수 테스트를 할 수 있는가?

매일매일 최신소스로 설치본이 자동으로 만들어지는가?
아마 이 부분이 다른 다섯가지보다 가장 재미있고, 쉽고, 뿌듯한 부분이 아닐까 한다. 왜냐하면, 자동으로 만드는 스크립트에는 나머지 것들에 대한 모든 것이 포함되기 때문이다. 그 끝은 관련자들에게 따끈따끈한 설치본의 URL과 빌드로그 및 릴리즈 노트를 메일로 전송하는 것이 될 것이다. 이렇게 기쁜 일은 프로젝트가 시작할 때 얼른 담당하고 남주지 말자. 만약 당신이 닥치는대로 공부하는 열정이 있는 사람이라면 이 일은 정말 남주면 안될 것이다.

적절히 crontab 을 운영할 것이고, 빌드 로그중에서 warning만을 추려내어 따로 보고해야할 지도 모르며, 하나의 소스로 여러 OS에서 동시에 빌드할 수 있어야하고, apache의 fancy indexing도 사용해야할 수도 있고, 가장 빨리 빌드하는 방법을 위한 여러 안건을 만들 수도 있고, cvs나 subversion 등의 tag, diff, merge 등에 대해 일가견이 생기게 되고 하여간 도무지 말로 할 수 없는 수 많은 것들을 빌드 시스템을 구축하면서 찾고 공부하게 된다.

임의의 바이너리가 어느 빌드에서 생성되었는지 알 수 있는가?
바이너리 안에는 대개 "static char []"형으로 된 $Id$가 들어가게 된다. 이것은 소스 정보만을 나타내게 되지만, ident라는 좋은 툴은 $String ...$ 형태로 되어 있는 모두 뽑아주므로 거기에 팀의 독특한 스트링을 버전 및 빌드 번호를 표시하는데 사용할 수 있다. 예를 들면
$Version: doorscan 2.2.1.1224 $
와 같이 Version 이라는 것을 사용할 수 있겠다. 만약 2.2.1.1224 와 같은 이름으로 소스 트리를 태깅을 해놓는 다면, 바이너리를 만드는데 들어간 소스들의 버전을 쉽게 뽑아낼 수 있게된다.

또는 반대로, 바이너리를 출시한 뒤 MD5 해시 값을 구해놓고 빌드시점에 모든 파일의 MD5 해시를 저장해 놓은 뒤 비교하여 구할 수도 있다.

바이너리들이 의존하고 있는 라이브러리들은 모두 알고 있는가?
ldd 를 이용한 검사인데, 개발도중 어느새 모르게 개발팀이 모르는 라이브러리가 들어갈 수 있다. 이것은 설치가 개발 장비에서만 제대로 될 수 있고, 정작 다른 곳에는 해당 바이너리가 없는 것으로 인해 설치후에 수행이 되지 않을 수 있다. 어딘가에 그 프로젝트 전체적으로 외부 라이브러리를 사용한다면 모든 배포되는 바이너리가 어떤 의존관계를 가지고 있고, 그 의존 관계에 대한 것은 선행작업이 필요함을 설치본 배포시에 적절히 명시해야한다.

바이너리안에 들어 있는 global symbol(nm 명령을 통해)들은 관리가 되고 있는가?
바이너리가 특히나 shared object(so) 파일이라면, 흔히 저지르기 쉬운 것이 단지 어떤 실행파일이 사용한다정도의 정보만을 가지기 쉽다. 즉, ldd에 의한 의존성만 확인하는 경우가 발생할 수 있는데, nm 을 통해서 정확히 외부에 노출시킬 것이 노출되고 있는지, 이전과 다르게 추가되거나 삭제된 것이 없는지 확인해야한다. shared object 의 생명은 global symbol(defined, undefined)l과 의존성이다. 이것이 적절히 빌드시점에 생성되거나 관리되지 않는다면, 나중에 업그레드시에 모든 바이너리가 배포되어야하는 상황이 발생할 수 있다. 그러면 왜 so로 분리했느냐!라는 소릴 듣게 될 수도 있다. 무작적 so로 쪼게는 것은 정말 삼갈일이며, 그것은 신발에 껌을 잔뜩 붙이고 줄넘기하는 것과 똑같다.

이전 설치본과 지금 설치본에서 변경사항은 자동으로 뽑혀 릴리즈 노트를 구성할 수 있는가?
릴리즈 노트라는 것은 새로운 버전을 출시할 때, 개발자들이 그간의 기억을 미루어 적어 내거나, 그동안 처리했던 버그 리스트를 정리하는 것이 아니다. 그것은 버그를 처리할 때마다 적어 놓은 간단한 노트들이 있어야하고, 그 노트들을 각 버전별로 뽑을 수 있는 상태로 관리되어야하는 것이다.

간단히는 cvs나 subversion을 쓸 때, 커밋로그를 이용해서 릴리즈 노트용을 적절한 규칙에 의해 적게되면, 태깅한 기간별로 구별하여 뽑아 낼 수 있다. 로그는 한 번 올리면 다시 수정못하는 것으로 아는데, cvs의 경우 cvs admin 명령으로 subversion의 경우 svn ps svn:log 조합으로 다시 고칠 수 있다. 로그는 커밋하는 시점외에는 다시 자세히 적을일이 없게된다. 대략 간단한 어플리케이션을 개발하는데, 5000 번 정도의 커밋이 되면 알파 릴리즈가 만들어지는데, 5000번에 대한 것을 누가 이후에 관리할 것인가.

부분 테스트를 위한 정보들은 버그트래킹시스템과 연결되어 꼭 필요한 인수 테스트를 할 수 있는가?
SCM(Software Configuration Management)이라는 영역이 있다. 검색을 하게 되면 개발 관련된 각종 도우미 툴들이 소개되는데, 가장 중요한 것은 소스를 소스만으로 두는 것이 아니라, 제기된 이슈나 버그, 그리고 고친 내역등을 유기적으로 결합하는 방법을 어떻게 구성할 것인가에 있다.
학교에서 갓 나온 수준으로 기업형 프로그램을 만들어야할 경우, 프로그램이란 몇달하고 마는 것이 아니라 계속 살아서 발목을 잡아 당기는 실로 무시무시한 존재와 같은 것을 느끼게 된다. 이런 문제를 해결하기 위해 버전 컨트롤 시스템을 도입하게되는데, 그 이후에도 문제는 또 발생한다. 버전 컨트롤 시스템이 단지 코드의 변화에 대한 것은 알려주지만, 코드가 변하는데는 이유가 있어야 하며, 그 이유의 근원에 대한 것은 동시에 수십 수백개가 관리되어야하고, 릴리즈 이후 코드에 변형을 가하는 모든 커밋에 대한 것이 테스트 대상으로 연결되어 문제를 재발하지 않고, 원하는 방향으로 적절한 계획을 가지고 나갈 수 있어야한다.

이상으로 간단히 빌드와 관련된 것을 살펴보았는데, 소스가 공개되어 개발되는 프로그램들을 잘 살펴보면, 빌드 및 그 관리와 관련된 상당히 많은 노하우들이 그 프로젝트안에 녹아 있고, 그 과정들이 단지 프로그래밍 실력으로만 시작하여 진행되는 것이 아니라는 것을 알 수 있다. 그들은 끊임없이 고객의 요구를 수용하고 있고, 버그를 찾고 있으며, 새로운 기능을 추가하고 있다. 이것이 모두 릴리즈를 어떻게 관리할지에 대한 것에 대한 것이 모두 오픈되어 진행된다.

어디서 개발을 하던지, 이상과 같은 빌드관리가 선행되지 않는다면, 조만간 누구도 손댈 수 없는 코드로 변신하고 말것이다.
  1. june8th 2005.04.04 09:46 신고

    잘 정리했네요.. 근데 이 문서의 버전은 어디에? ㅎㅎ

  2. 2005.04.04 10:38 신고

    ㅋㅋ.. 요건 버전관리 대상이 아닌 릴리즈지...
    설치해서 배포할까? ^^;

  3. 석영 2005.04.17 14:21 신고

    이 쪽은 정말 관심이 많은데, Windows 환경에서 소스 자동 빌드와 버전 배포를 위해 사용할 수 있는 툴들 또는 관련 리소스들이 있는 사이트가 있으면 알려주겠어요?

  4. 2005.04.18 07:42 신고

    아하.. 그것참.. 저는 윈도우쪽 환경에서는 많이 고민하지 못해서, 저도 찾아봐야해요..

Subversion : http://subversion.tigris.org/
TortoiseSVN: http://tortoisesvn.tigris.org/
poedit: http://www.poedit.org/translations.php

나는 요 세가지의 한국어 문자열을 관리하고 있는데, 어떤 소프트웨어가 맘에 들면 번역을 시도해보는 것은 그 프로그램을 깊이 이해하는데 더 도움이 된 것 같다. 가장 활발히 하는 것은 TortoiseSVN인데, 그 이유는 일주일에 한 번 씩 번역 상태가 메일로 오고 각 언어별 번역율이 그래프로 나오는 것이 약간의 경쟁을 유도하게한다.

subversion은 일주일에 한 번정도 모든 언어들의 번역상태가 메일링리스트로 전송되어 온다.
Translation status report for revision 13587 (trunk/)

============================================================================
Status for 'de.po': in repository
Passes GNU msgfmt --check-format
Statistics:
0 obsolete
82 untranslated
1238 translated, of which
48 fuzzy
----------------------------------------------------------------------------
Status for 'es.po': in repository
Passes GNU msgfmt --check-format
Statistics:
0 obsolete
94 untranslated
1226 translated, of which
106 fuzzy
----------------------------------------------------------------------------
Status for 'ja.po': in repository
Passes GNU msgfmt --check-format
Statistics:
0 obsolete
75 untranslated
1245 translated, of which
121 fuzzy
----------------------------------------------------------------------------
Status for 'ko.po': in repository
Passes GNU msgfmt --check-format
Statistics:
0 obsolete
79 untranslated
1241 translated, of which
134 fuzzy

poedit도 번역 상태가(tortoise svn처럼) 웹에는 계속 올라오는데, 메일을 안보다 보니 내가 들어가서 보기전까지는 잘 모르게 된다.(실로 지금까지 한 번 보내고 말았는데, 이 놈의 게으름..)

(내가 관심있는 요 세 프로그램에 대한 독일어버전도 TortoiseSVN의 번역관리담당인 뤼베 옹켄이라는 한 사람이 한다.)

사실 개인적으로 svn을 사용하면서 cvs의 사용이 극도로 줄어들게 되었고, 그에 따라 관심의 영역이 줄어 든 것도 사실이다. (cvs 프로젝트도 observer로 등록되어 있기는 하다마는) 번역하면서 위세를 떨칠것도 아니고, 관심의 끊임없는 표현인데, 그에 따른 적절한 책임감이 소프트웨어를 빛나게하는 것 같다.

난 일주일에 한 번, 번역에 시간을 들여 커밋 또는 메일링 리스트에 올리는데, 대개 주말에 한다. 번역을 하게 되면서, 프로그램을 작성할 때 번역자를 고려하는 설계와 누구나 번역을 쉽게 할 수 있도록 하는 방법에 대한 고민을 많이 하게 되었고, 나와 같이 패키지 작업을 하는 것이라면 더욱 도움이 많이 된다.

po 포맷이라는 전통적인 Unix 기반 다국어 처리용 포맷이 있는데, 요놈의 구조가 간단하고 그 editor들 또한 훌륭하여 프로그램을 모르는 사람을 번역하는데 고용할 수 있으므로 더욱 그 활용도가 높다.

Subversion과 poEdit 프로젝트는 전통적인 방식 그대로 po를 compile한 mo 파일을 어플리케이션에서 핸들링한다. 즉 gettext라는 라이브러리에서 최적화되어 메모리 맵된 파일 입출력을 통하여 사용되는데, 요 Tortoise SVN이라는 녀석은 gettext library를 사용하지 않고, po 포맷을 단지 우리같은 지역화하는 사람들과의 통신도구로만 사용하는데 그 재미가 있다. 그 개발팀은 restext.exe 라는 MS 윈도우용 프로그램을 이용하여 po 파일을 dll로 만들어준다. 마치 mo 파일 만들듯이 Tortoise SVN용 리소스 DLL을 만들며, 현재 아무 문제 없이 Tortoise SVN의 언어 확장팩으로 배포되고 있다.

난, 여기에 힌트를 얻어 회사에서의 프로젝트에 po 포맷을 도입했는데, 이로써 사내에서는 po 포맷을 이용하여 다국어 기반의 업무 프로세스를 만들었다는데 그 의의가 있다. 즉, 소스와 전혀 상관없이 po 파일로 기술문서팀과 이야기하고 개발팀에서는 po 포맷을 자바스크립트의 연관 배열로 바꾸는 스크립트를 간단히 제작하여 웹관리툴의 다국어 지원을 꾀하였다.

약 1000개의 문자열로 된 웹관리툴이 po 기반의 다국어 지원을 한다는것이 신선하지 않은가? strings.js 라는 그 자바 스크립트는 모든 웹페이지가 link하는 형태로 load하므로 네트웍 트래픽은 한 번만 일어나게되며, 화면에 약 30개 정도의 문자열이 그려지게 되는데, 1000개정도의 연관배열을 뒤지는데는 그다지 많은 컴퓨팅 파워를 소모하지 않는다.

다국어를 지원하는 방법은 여러가지겠지만, 될 수 있으면 많이 사용하는 방법을 최대한 이용하는 것이 생산성과 직결된다는 생각을 상기하며 이만.
[CODE]$ cat a.c #include <stdio.h> int noinit_global_var; int init_global_var = 0; static int static_var; static int init_static_var = 0; int func() { static int func_static_var; return 0; } $ nm a1.o 00000000 T func 00000008 b func_static_var.0 00000000 B init_global_var 00000004 b init_static_var 00000004 C noinit_global_var 0000000c b static_var $ nm a2.o 00000000 T _Z4funcv 00000010 b _ZZ4funcvE15func_static_var 00000004 B init_global_var 0000000c b init_static_var 00000000 B noinit_global_var 00000008 b static_var $ nm -C a2.o 00000000 T func() 00000010 b func()::func_static_var 00000004 B init_global_var 0000000c b init_static_var 00000000 B noinit_global_var 00000008 b static_var[/CODE]


C와 C++에서 초기화 되지 않은 전역 변수를 다루는데 gnu c/c++ 컴파일러는 각각 다른 코드를 만들어 낸다. 각각을 보면 C에서는 "C" 즉 COMMON으로 만들고 C++에서는 "B" 즉 BSS 영역의 데이터로 만들어 낸다. C와 B의 차이를 nm의 info 페이지에서 찾아 보면,

[CODE]$ info nm `B' The symbol is in the uninitialized data section (known as BSS). `C' The symbol is common. Common symbols are uninitialized data. When linking, multiple common symbols may appear with the same name. If the symbol is defined anywhere, the common symbols are treated as undefined references. For more details on common symbols, see the discussion of -warn-common in *Note Linker options: (ld.info)Options. [/CODE]

이런 차이가 있으니, C 에서는 두 개의 파일이 초기화하지 않은 전역 변수를 외부로 노출할 경우 아무런 문제없이 링크되나 C++에서는 그렇지 않게 된다.
[CODE]char ch; while( (ch = fgetc( f )) != EOF ) { printf("%c", ch ); }[/CODE]

언뜻보기에는 맞는 것 같이 보인다. 하지만, 여기에는 isprint 못지 않은 두려운 버그가 숨어 있다.

fgetc 의 원형은 다음과 같다.

[CODE]int fgetc( FILE * );[/CODE]

fgetc의 return 값이 int 란다. 그리고, 문자하나를 되돌리는 함수라니..

각설하고 위 코드는 다음과 같아야한다.

[CODE]int ch; while( (ch = fgetc( f )) != EOF ) { printf("%c", (char) ch ); }[/CODE]

fgetc의 설명을 보면, 파일의 끝이나 오류를 만났을 때 EOF를 되돌린단다. unsigned char 로 표현할 수 있는 문자의 범위를 벗어나는 값으로 EOF가 정의되어 있지 않는한 파일에서 EOF와 동일한 문자값을 읽었을 때 이것이 파일의 끝이 아님에도 불구하고 끝으로 해석하는 버그가 좋아하는 상황이 벌어지게 될 것이다.

처음 코드에서는 강제로 char 변수에 받았으니, 문자 중에 어떤 값은 EOF로 해석되는 경우도 생기는 것은 당연하고, 따라서, fgetc의 리턴값은 파일에서 한 문자를 읽는것에만 흥분한 나머지, 오류 리턴을 제대로 판단하지 못하게 되는 것이다.
따라서, EOF인지 확인한다음 char로 캐스팅하여야 정상적인 사용법이 되는 것이다.

실제 EOF는 많은 구현에서 -1 로 정의되어 있다.
char ch = 'X';

위와 같이 되어 있을 때, 다음과 같이 사용하는 것이 옳은 것이냐하는 것인데,

if( isprint( ch ) )
{
blah;
}

경고감이다. 왜냐하면, isprint의 원형은

[CODE]int isprint (int c);[/CODE]

이기 때문이며, 여기에는 isprint에 넣는 인자의 철학과 우리가 흔히 사용하는 문자형 변수의 차이에서 오는 괴리감이 있는 것이다.

int 는 char가 표현할 수 있는 것보다. 일반적으로 더 많은 범위를 받을 수 있는데 (sizeof( char ) == sizeof( int ) 인 구조를 제외하면 항상 그렇지 않는가?) 그러면서도 signed 형이라는 것이다. 그런데, char 는 명시적으로 signed char, unsigned char이라 쓰지 않는한 컴파일러의 디폴트 값을 따른다.
그 디폴트 값이 signed라면, 위 코드는 부호확장이라는 개념으로 캐스팅이 일어날 것이다. 일반적으로 문자 코드 값은 음수가 없는데도 char 만 사용함으로 signed char로 취급되고, 이는 unsigned char 로 표현할 수 있는 0~255까지의 코드를 -128~127로 해석하여 isprint 함수에 전달하게 될 것이다. 이는 개발자의 뇌리 속에서 원하지 않았을테고, isprint도 사실 원하지 않을지 모른다.

컴파일러에 따라 이러한 char에서 int로의 암시적 캐스팅을 경고하는 경우도 있으므로 참고하시되, 유독 short 에서 int 로의 확장은 아무 문제가 없는데, char 에서 int 는 한 번쯤 생각해보아야한다.

따라서, isprint 같이 int로 문자를 받아서 평가하는 함수와 어울리는 곳에서는 unsigned char 로 받아서 처리하는 센스도 필요하다.
* GNU GCC versus Sun's Compiler in the SPARC Platform
::http://www.osnews.com/story.php?news_id=5830&page=3
* Are 64-bit Binaries Really Slower than 32-bit Binaries?
::http://www.osnews.com/story.php?news_id=5768
* Solaris 64-bit Developer's Guide
::http://docs.sun.com/app/docs/doc/806-0477
* Compiler Usage Guidelines for 64-Bit Operating Systems on AMD64 Platforms
::http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/32035.pdf
* Intel 386 and AMD x86-64 Options
::http://gcc.gnu.org/onlinedocs/gcc-3.4.3/gcc/i386-and-x86_002d64-Options.html
* Porting InteㅣApplications to 64 bit Linux on POWER
::http://www-1.ibm.com/servers/enable/linux/pdfs/intel_ppc64.pdf
* AIX 5L Porting Guide
::http://publib-b.boulder.ibm.com/Redbooks.nsf/RedbookAbstracts/sg246034.html?Open
* Large File Support in Linux
::http://www.suse.de/~aj/linux_lfs.html
* Porting x86 Linux device drivers to AMD64 Technology
::http://www.amd.com/us-en/assets/content_type/DownloadableAssets/Porting_x86_Linux_device_drivers_to_AMD64_Technology.htm
* Proceedings of the GCC Developers Summit: Porting to 64-bit Linux systems
::http://zenii.linux.org.uk/~ajh/gcc/gccsummit-2003-proceedings.pdf

  1. 함수의 프로토타입을 꼼꼼히 분석하라 : 함수의 프로토타입에는 그 함수가 뭘하는 것인지에 대한 정보의 90%가 들어 있다.
  2. const 형식이 어떤 것인지 모두 이해하고 있어라 : 함수 인자, 변수 선언, 멤버 함수 맨 뒤.
  3. static 형식이 어떤 것인지 모두 이해하고 있어라 : 변수 선언, 함수 선언, 멤버 함수
  4. 오브젝트 파일안에 뭐가 들어 있는지 알고 있어야한다. : nm, objdump, readelf, dumpbin.exe, depends.exe 등의 유틸리티가 도움이 된다.
  5. 소스를 코딩하고나면 오브젝트 코드가 어떻게 생기는지 알고 있어야한다.
  6. 스택에 쌓이는 순서를 상상하라
  7. 커널레벨과 사용자레벨의 차이와 그 전환은 어떻게 일어나는지 알고 있어야한다.
  8. 디버깅 툴(브레이크 포인트, 변수 내용 보기, 시스템 콜 트레이싱)의 작동원리를 알고 있어라.
  9. 환경변수가 어떻게 저장되는지 알고 있어라.
  10. fork에서 유지되는 것과 유지되지 않는 것에 대해 알고 있어라.
  11. callback 함수의 개념에 대해 알고 있어라
  12. -fPIC로 주어지는 relocatable object의 원리에 대해 알고 있어라
  13. 호출 스택에 대한 구조를 알고 있어라
  1. Delight 2005.06.21 12:32 신고

    좋은글 많이 보고 가요

  2. doodoo 2006.04.12 02:47 신고

    9번 글은 제가 항상 궁금하던 것인데...어디서 좀 찿을길이 없을까요? 무슨 글자를 넣어서 검색을 해보라는 둥...그런거...

    • 주인 2006.04.12 03:34 신고

      hex dump를 만들어서 argv 가 가리키는 곳부터 1kB 정도를 출력해보세요.

      어디서 찾긴 힘들것 같습니다.

    • 최호진 2006.04.12 11:08 신고

      bss code stack env
      위 단어를 모두 넣어 검색해보세요.

diskmgmt.msc : 디스크 관리자
services.msc : 서비스 관리자
devmgmt.msc : 장치 관리자
fsmgmt.msc : 파일 공유 관리자
lusrmgr.msc : 로컬 사용자 관리자
gpedit.msc : 그룹정책
1. 서문

2. Local Scheduler / System Scheduler

3. Symmetric Job Unit / Asymmetric Job Unit

4. Process / Thread (per client)

5. Pre-spawned / Post-spawn (per connection-request)

6. Reuse / One-time use (job unit life cycle)

7. Configurable / Fixed job

8. Single port listening / Multiple ports listening

9. Level detected triggering / Edge detected triggering

10. Asynchronous / Synchronous Handling




1. 서문



서버를 설계할 때 다음 같은 요소를 가지고 선택하게 된다.



  1. local-scheduler / system-scheduler (non-block socket handler)
  2. symmetric / asymmetric job unit
  3. process / thread (per client)
  4. pre-spawn / post-spawn (per connection request)
  5. configurable / fixed job (job unit modifiablity)
  6. reuse / one-time use (job unit life cycle)
  7. single port listening / multiple ports listening
  8. edge detected triggering / level detected triggering


이들은 대개 서버 설계 초기에 주로 선택하나, 때로는 중간에 그 모델을 바꾸어 설계할 수도 있다. 그러나 어떤 것들은 임기응변식으로 서버 설계 변경이 가능하나, 어떤 변경은 처음부터 완전히 다시 작성해야하는 경우가 생기기도한다. 서버를 설계할 때 내외적인 상황에 대하여 고려해야할 사실관계들을 살펴보고 적절한 선택을 위해 정리해보고자 한다. 위에서 나열한 요소들은 하나의 서버를 설계할 때 부분적으로 선택되어지므로, 어떤 상황에 대한 서버를 설계할 것인지를 충분히 고려하지 않으면, 설계 변경시 상당한 충격이 있을 것이다.


2. Local Scheduler / System Scheduler



Non-block I/O, Thread Pool 개념을 사용할 것인가?

Process, Thread 전담형으로 만들 것인가?



  • 선택의 동기

  1. 흔히 non-block socket을 쓸 것이냐, block socket을 쓸 것이냐로 구분하기 쉬운 것을 job scheduler(작업 스케쥴러) 혹은 task switching 입장에서 구분해보았다. 그 만큼 non-block I/O를 통해 처리하는 서버에서는 작업 스케쥴하는 비용이 크게 고려되어야한다는 것이며, 프로세스나 쓰레드의 스케쥴링에 들어가는 시간보다 클라이언트 요구를 처리하기 위해 들어가는 시간에 집중하도록 설계하는 것을 의미한다.
  2. 시스템 스케쥴러를 사용한다는 것은 여러접속의 요구 처리를 프로세스 혹은 쓰레드 스케쥴링에 넘겨 처리하겠다는 것이다.
  3. 스케쥴러에 들어가는 비용을 아까워하는 경우에 Local Scheduler(로컬 스케쥴러)를 선택하게 된다. 예를 들어 데이터의 양도 많고 동시 접속도 많은 경우를 생각해보자, 동시에 1000개 이상의 접속이 생기고 이것들을 프로세스 1000개로 운영하는 것보다 2 개정도의 프로세스가 500개씩 나누어 처리하면, OS의 스케쥴에 해당하는 비용을 크게 줄일 수 있을 것이다. 여기에서 500개의 접속을 하나의 프로세스에서 효과적으로 처리하기 위하여 로컬 스케쥴러라는 말을 도입하였다.
  4. 로컬 스케쥴러라는 것은 User level thread 수준의 복잡도를 요구하는 것이 아니며, non-block I/O를 처리하거나 Thread-pool을 도입하여 소켓당 State machine을 잘 운용하는 수준의 스케쥴러를 말한다.



  • 이점 및 주의점

  1. Local scheduler는 하나의 프로세스에서 작업 분배를 논리적으로 구분한 것일 뿐, OS가 보기에는 하나의 프로세스에 불과하다. 즉, 간단한 User-level thread라고 생각해도 좋을 정도이다. 따라서, system call을 수반하는 무거운 context switching을 막을 수 있으며, 작업들간의 분배가 상당히 가벼운 것에 그 이점이 있다.
  2. 주의할 것은 non-block 소켓을 다룰때는 특히 대량의 접속에 대한 처리가 있을 때, 프로세스당 열 수 있는 최대 디스크립터 수에 도달할 가능성이 많다. 이 경우 똑같은 일을 하는 프로세스가 listen port를 공유하여 경쟁적으로 클라이언트를 접수하는 pre-forked 방식 서버를 사용하여야한다.
  3. 모든 작업은 하나의 프로세스내에 남기 때문에 쓰레드 프로그래밍과 같은 (함수 재진입 문제 등) 수준의 주의를 요한다.



  • 구현에 따른 고려사항

크게 영향을 받는 요인은 프로세스당 최대 열 수 있는 디스크립터 수와 CPU의 개수이다. 후자에 대해서는 CPU 개수의 두 배정도에서 작업전환 비용을 절감하는 이점을 최대화 시키는 것이 경험적으로 알려져 있다. 즉, non-block I/O를 처리하는 쓰레드 풀안의 쓰레드 개수는 1 CPU machine에서 두 개정도가 적당하며, 그 이상 늘여도 성능향상이 월등히 좋아지지는 않는다는 것이다.


3. Symmetric Job Unit / Asymmetric Job Unit



생성되는 프로세스/쓰레드가 모두 같은 일을 하는가?

생성되는 프로세스/쓰레드 마다 역할이 분배되어 있는가?



  • 선택의 동기

  1. 프로세스 혹은 쓰레드가 다수 만들어 질 때, 이들은 모두 같은 일을 하거나(대칭적 작업단위) 상호 협조(비대칭 작업단위)를 하는 모델로 만들어진다.
  2. 예를 들면, HTTP Proxy 설계에 하나의 접속건에 대하여 하나의 쓰레드가 만들어진다면, 이 쓰레드 하나가 클라이언트를 요구를 파싱하고 접속해야할 서버에 접속하며, 서버의 응답을 다시 릴레이하는 일련의 과정을 전담하도록 설계되거나, 두 개정도의 쓰레드로 나누어 하나는 클라이언트와 접속을 담당하고 다른 하나는 서버쪽 접속을 담당하는 형태로 설계될 수 있다. 그 외에 주기적으로 가비지 콜렉팅을 하는 쓰레드도 만들수 있고, 로그를 분리하기 위한 쓰레드도 만들어 질 수 있다.
  3. 대칭적인 서버는 모든 프로세스 혹은 쓰레드가 동일한 일을 하는 작업 단위로 만들어져 접속된 클라이언트의 요구사항을 전담하여 처리한다.
  4. 하나의 처리가 짧은 응답시간을 갖지 않는 경우 처리를 여러 단계로 나누어 각 단계마다 복잡할 수록 여러 프로세스 혹은 쓰레드로 나누어 처리시킬 수 있다.이는 경험적인 프로세스 수 조정 과정을 통해 병목이 생길 수 있는 단계에 여러 프로세스를 둘 수 있는 우아함을 지원하게 된다.



  • 이점 및 주의점

  1. 작업단위 쪼개어 만들어지므로 설계와 구현에서 고립화가 쉽다. 재사용이 가능하므로 쓰레드나 프로세스 수가 경제적으로 생성되며, 그 라이프 사이클도 상당히 경제적이다.
  2. 작업단위가 추상화되면, 업그레이드나 작업단계 추가등이 상당히 명료하다.
  3. 요청이 여러 작업단위를 뛰어 다니므로 중간에 분실될 우려가 있다.
  4. 단순한 요청사항인 경우 굳이 작업단위를 나누게 되면 오히려 복잡한 설계가 될 수 있다.


4. Process / Thread (per client)



하나의 접속에 대하여 한 프로세스를 만들어 처리할 것인가?

하나의 쓰레드를 만들어 처리할 것인가?


작업개체를 프로세스로 할 것인가?

작업개체를 쓰레드로 할 것인가?



  • 선택의 동기

이 문제는 프로세스를 생성하는데 드는 비용이 많다는 것에 기초한다. 전통적으로 fork를 하는데 들어가는 비용이 크기 때문에, vfork라는 개념도 생기며, 뒤에서 보게될 prefork라는 개념도 생기게 된다. 일이 발생했을 때, 무거운 시스템콜을 될 수 있으면 줄이려고 대신 thread를 택할 수 있다.

  • 쓰레드의 이점과 주의점

  1. 이점은 모든 쓰레드가 메모리를 공유하면서 생기는 클라이언트간 자료공유가 쉬운 것에 있다. 또한 많은 OS에서 쓰레드 생성비용이 프로세스보다 작다.
  2. 문제는 하나의 쓰레드는 완전무결 해야하는데 있다. 즉, 리소스 (메모리, 디스크립터 등)가 절대 새지 않아야하며, 쓰레드 자체가 치명적인 오류를 일으켜 프로세스 전체에 영향을 줘서는 안되는데 있다. 반면, 접속이 해제됨과 동시에 프로세스가 종료되는 경우 리소스는 자동으로 해제되므로 쓰레드 기반보다는 견고하다.

  • 구현에 따른 고려사항

  1. 리눅스의 경우 쓰레드의 생성이나 프로세스의 생성이 그다지 비용차이가 많지 않다. 즉, 리눅스의 쓰레드는 좀 무거운 편이다. 따라서 fork에 들어가는 비용을 고려하는 것만으로 쓰레드를 선택하지는 않는다.
  2. 쓰레드의 경우 User level 쓰레드와 Kernel level 쓰레드가 있다. 이 경우 User level일 때의 고려사항은 자칫 하나의 쓰레드에서 block 상황에 빠지게 되어 프로세스가 멈추게 되는 상황이다.
  3. 쓰레드의 경우 하나의 쓰레드가 차지하는 stack의 크기가 있으므로, 쓰레드 생성시 적절한 스택의 크기를 정하는 문제가 발생한다. 스택의 크기에 따라서 생성되는 쓰레드 최대 수가 달라지기 때문이다.
  4. 쓰레드의 경우 하나의 프로세스가 열 수 있는 최대 프로세스 개수에 제한이 있고 그것에 도달하는 경우는 어쩔 수 없는 한계에 도달한 것이라 생각하고 변경해야하는 경우가 있다.


5. Pre-spawned / Post-spawn (per connection-request)



connection 요구가 있을 때, 새로운 작업개체를 만들고 수신하여 처리결과를 전송할 것인가?

미리 만들어진 작업개체가 connection 요구를 accept하고 수신하여 처리결과를 전송할 것인가?



  • 선택의 동기

Process 생성에 들어가는 비용이 많이 들기 때문에, 사전에 Process를 준비하여 빠른 응답을 줄 수 있도록한다.



  • Pre-spawned의 이점과 주의점

  1. 접속을 전담하는 형태로 작성되며, 대개 전담하는 모델이 이미 접속된 소켓에 대하여 하는 것과는 달리, 접속요구를 accept하는 것부터 전담하게 된다.
  2. 동시에 두개의 port를 listen하는 경우 accept하기전 select를 사용하고, 이 select에 참여하는 작업개체(Process, thread)를 하나로 한정하기 위해 semaphore나 mutex 등을 둔다. 이는 accept 경쟁에 실패한 작업개체들이 한 포트의 accept에 멈춰있어 다른 포트에 accept할 기회가 상실되는 것을 막기 위함이다.


6. Reuse / One-time use (job unit life cycle)



접속을 전담하는 작업개체가 접속이 종료되면 같이 종료되나?

접속을 전담하는 작업개체가 접속이 종료되면 다른 접속을 받기 위해 대기하나?

몇번의 접속을 전담하고나면 종료되나?



  • 선택의 동기

  1. 작업개체의 생성에 들어가는 비용을 줄이기 위해 재사용하도록 한다. 주로 Pre-spawned(Pre-forked) 방식에서 사용된다.



  • reuse 모델에서의 이점과 주의점

  1. 이전 접속의 데이터가 다음 접속에 영향을 주어서는 안된다.
  2. 한 클라이언트의 접속/응답 시간이 작은 경우에는 효과적이나, 그렇지 않은 경우에는 고려하기 어렵다.



7. Configurable / Fixed job



접속을 받아 실제 응답을 주는 것은 고정되어 있나?

접속을 받아 실제 응답을 주는 작업이 바이너리 릴리즈 후에 추가되거나 수정될 수 있는가?



  • 선택의 동기

서비스 모델이 확장성은 고려되지 않는 경우 모든 작업은 컴파일되어 릴리즈 되지만, 버전업이 잦거나 접속이 종료되지 않은 상황으로 hotfix를 해야하는 경우를 고려하면, 선택해야한다.



  • 이점과 주의점

  1. 외부 모듈이 실행되는 형태로 작성되므로, 보안상 허점이 발견될 수 있다.
  2. 안정성이 해결된 바이너리라는 확인작업 없이 hotfix하면, 기존의 서비스도 중지될 수 있다.
  3. 외부 모듈을 통해 인증을 거칠 수 있다.



  • 구현시 주의점

  1. 구현을 inetd와 같은 방식으로 할 경우 accept후 fork/exec를 사용한다.
  2. 구현을 shared object 방식으로 할 경우 외부 object를 dlopen 한다.


8. Single port listening / Multiple ports listening



서비스 포트가 하나인가?

서비스 포트가 하나 이상인가?



  • 선택의 동기

외부에 두 개의 port를 보임으로서 다른 프로토콜 또는 다른 주소를 하나의 서비스안에서 구현한다.



  • 이점과 주의점

  1. 하나의 바이너리만을 배포함으로써 서비스를 간소화시킬 수 있다. 대표적으로 inetd, apache web server
  2. accept 경쟁이 일어날 경우 한쪽 port로 몰려 "port 왕따 현상"이 일어나지 않아야한다.



  • 구현 참고

  1. 서비스 구분은 getsockname을 통해서 local port를 구함으로써 알 수 있다.


9. Level detected triggering / Edge detected triggering



수신 요청 신호를 확인하여 응답할 것인가?

요청 버퍼를 확인하여 응답할 것인가?



  • 선택의 동기

시스템에서 제공하는 전통적인 select, poll 방식의 감지는 동시에 참여하는 descriptor 수가 많아질 경우 scan하는 비용이 많이 들어간다. 따라서, 수신요청이 있을 때 바로 처리하도록한다.



  • 이점과 주의점

  1. 대량의 접속을 처리해야하는 경우 서비스를 해야할 시점을 선택하는 문제를 접속 혹은 수신 통지 서비스를 이용하므로 응답시간을 단축할 수 있다.
  2. 한번의 수신 통지에서 버퍼에 남겨두는 것이 없도록 모두 비워주지 않으면, 버퍼 오버플로가 발생하여 서비스가 정지할 수 있다.
  3. OS에 의존하는 서비스를 이용하므로 이식성을 포기해야한다.



10. Asynchronous / Synchronous Handling



요청한 작업을 Callback함수를 통해 마무리 할 것인가?

요청한 작업이 종료될때까지 기다릴 것인가?
요청한 작업이 종료되었는지 확인하여 처리할 것인가?



  • 선택의 동기

  1. read/write 시스템콜을 사용하는 전송요청은 전송이 마무리 될때까지 대기하여 시간을 소비하게 된다. 이 대기 시간을 의미있게 사용하기 위해, 수신/전송 요청과 실제 수신 데이터 도착 / 송신 완료가 이루어지는 시점을 분리하여 그 사이에 다른 일을 하도록 한다.
  2. 사용자의 수신/전송 요청에 대한 결과를 확인하는 것과 요청에 대한 커널의 응답이 비동기적으로 한다.
  3. read/write 라는 하나의 명령을 사용하지 않고, asynchronous I/O 구현 Library의 request, callback 메커니즘을 사용한다.



  • 이점과 주의점

  1. 전통적인 signal-base async I/O는 signal 이라는 상대적으로 무거운 시스템콜을 통하여 일어나므로 매 처리가 signal handling 상황이라는 것에 주의해야한다.
  2. 대개 라이브러리에 의존하는 서비스이므로 이식성을 포기해야한다.

+ Recent posts