스택 탐색을 통한 디버깅 본문

Programming

스택 탐색을 통한 디버깅

halatha 2009. 9. 23. 11:08
출처: 열씨미와 게을러의 리눅스 개발 노하우 탐험기
2009/08/17 - [Programming] - diff, find, md5sum, patch
2009/08/17 - [Programming] - find, grep, ctags, cscope, global
2009/08/17 - [Programming] - shared library, strace, ldconfig, ldd, gcc, strings, od, nm, c++filt, readelf
2009/09/14 - [Programming] - configure
2009/09/21 - [Programming] - 자동화된 빌드 시스템 구축
2009/09/21 - [Programming] - 자동화된 빌드 시스템 구축 (2)
2009/09/21 - [Programming] - 숨겨진 1인치의 의존성을 찾아서 - make
2009/09/22 - [Programming] - 메모리 디버깅을 위한 친구
2009/09/22 - [Programming] - 프로그램을 동적으로 추적하는 도구

스택의 사용 목적
  • 서브 루틴(활성화된 스택)이 끝나면 돌아갈 곳 저장
  • 서브 루틴 호출 직전에 서브 루틴이 끝나고(프로그램 카운터에 대입할) 돌아올 주소를 호출 스택에 넣는 방법이 가장 일반적
  • 호출 스택은 스택 구조이므로 서브루틴 호출 순서에 맞춰 차례로 LIFO 방식으로 호출 스택에 돌아갈 주소를 넣으므로 중첩된 서브루틴 호출이 가능
호출 스택에 들어가는 정보
  • 서브루틴 종료 직후 돌아갈 주소
  • 자동 변수(지역 변수): 힙보다 스택에 저장하는 변수가 공간 할당 측면에서 효율적인 이유는 스택 영역은 이미 정적으로 잡혀 있기 때문에 단순히 스택 포인터만 바꾸면 되기 때문
  • 매개 변수: 일부 레지스터가 많은 CPU의 경우 매개 변수를 레지스터에 넣어 전달하기도 하지만 표준 x86 PC의 경우에는 스택으로 전달
호출 스택은 활성 레코드라고 브루기도 하는 스택 프레임 여러 개를 포함하는 구조로 형성
스택 프레임
  • 아직 return으로 종료되지 않은 서브 루틴 호출에 대응하는 기본 단위
  • 서브 루틴이 호출될 때마다 호출 스택에 쌓임
    필요한 레지스터, 돌아갈 주소, 자동 변수, 매개 변수로 구성
  • 크기가 제각각인 이유는 서브루틴마다 자동 변수 개수, 매개 변수 개수가 다르기 때문
  • 스택 프레임이 기준 크기의 정수 배가 되지 못하므로 정수 색인이 아니라 실제 스택 프레임 메모리 주소를 가리키는 (스택) 프레임 포인터라고 부르는 레지스터가 존재(스택 포인터와 프레임 포인터는 다름)
  • 프로그램 실행 중에 서브 루틴 호출에 따라 스택 프레임이 추가(wind)되었다 삭제(unwind)되면서 크기가 변동
문제가 발생한 경우 호출 스택을 뒤져 특정 함수 내부의 자동 변수 값을 얻고 이 함수를 호출한 부모 함수 호출 체인을 따라가면서 문제 원인을 파악해야 함

ABI(Application Binary Interface)
  • application program과 OS 사이, application program과 library, application program과 object file 사이에서 일어나는 저수준 interface를 정의한 규약
  • 동일한 ABI를 사용해서 compile한 object file을 변경 없이 link할 수 있도록 한다
  • 이론적으로는 OS가 달라도 동작하는 platform과 ABI만 동일하면 다시 compile을 하지 않고서도 object file끼리(혹은 library와) linking이 가능
  • function parameter variable과 return value를 처리하는 호출 관례, 시스템 호출 번호, object file과 library 형식을 정의(API는 parameter variable type, return value type을 정의해 source code 수준에서 호환성을 유지)
  • platform 별로 차이가 나므로, x86, x86-64, power PC, ARM, MIPS와 같은 CPU마다 정의하는 방식이 다름. 예를 들어 x86과 x86-64의 경우 parameter 전달 방식과 return value를 다음과 같이 처리
    • x86 - 모든 인수를 stack에 push - %eax register
    • x86-64 - 첫 6개 인수를 왼쪽에서 오른쪽으로 %rdi, %rsi, %rcx, %r8, %r9에 넣고 나머지는 stack에 push - %eax register
  • register를 더 많이 사용할수록 함수 호출 성능이 좋아지므로 되도록 ABI가 지원하는 register 숫자(예: ARM, MIPS - 4개, power PC - 8개) 범위 내에서 function parameter 개수를 정의해야 한다
gdb로 스택 정보 살펴보기
gdb에 undo 기능은 없지만 함수 호출 체인을 따라 움직이는 기능은 있다

foobar.c

up / down: 함수 호출 체인을 따라 이동
where full: 함수 호출 체인에 포함된 지역 변수 출력

 명령 설명
backtrace / bt / where
모든 스택 프레임 역 추적 정보 출력
all을 뒤에 붙이면 스택(자동) 변수 출력
down [stack frame 이동 개수]
한 단계 아래 stack frame 선택과 정보 출력
up [stack frame 번호 이동 개수]
한 단계 위 stack frame 선택과 정보 출력
frame [stack frame 번호]
stack frame 선택과 정보 출력
select-from [stack frame 번호]
stack frame 선택

출력되는 정보 함수 이름, 인수, 파일 이름, 행 번호의 경우 -g option을 통해 debugging 정보 내부에 함수 이름이 있고, gdb는 program counter register와 이 주소가 가리키는 memory 영역에 올라온 실행 파일이나 library debugging 정보를 살펴서 program counter를 함수 이름 파일 이름 행 번호로 변환
gdb는 현재 frame 내부에 속한 지역 변수만 볼 수 있기 때문에 다른 함수에 속한 지역 변수를 살펴보려면 frame을 변경해야만 한다. 함수 bar에 진입한 시점에서 stack frame 3개가 program foobar의 호출 스택을 구성하며(0 - bar, 1 - foo, 2 - main), bar가 종료되면 두 개가 구성한다(0 - foo, 1 - main).

gdb로 스택 프레임 자세히 살펴보기
&를 사용해서 자동 변수와 매개 변수 주소를 보면 stack frame의 윤곽을 구성할 수 있다. gdb에서는 info frame이라는 frame 관련 정보를 세부적으로 출력하는 명령이 있다

stack.c

info frame
위의 정보에 따라 살펴보면 stack frame은 다음과 같이 구성이 되어 있다
(낮은 주소)
자동 변수 d: 0xbfcd10c4
ebp: 0xbfcd10c8
eip: 0xbfcd10cc
매개 변수 c: bfcd10d0
ebp/eip: function을 호출하면 stack pointer와 program counter가 변하기 때문에 나중에 원래 호출된 곳으로 돌아갈 때 복귀시키기 위해 저장. 즉 각 함수 호출마다 매개 변수, 자동 변수 뿐만 아니라 %ebp, %eip를 별도로 저장
up을 실행해 stack frame을 하나 위로 이동한 후의 정보를 보면 %ebp, %eip가 바뀐 것을 볼 수 있다
단 program이 심각한 오동작을 일으켜 stack 영역이 망가지면 gdb도 stack frame을 찾을 수가 없어 이 방법을 사용하는 것이 불가능하다

기타
앞에서 x86에서 매개 변수를 전달하기 위해 stack을 사용한다고 했으나, compiler에 따라 register를 사용하기도 한다. MS의 Visual C++의 경우 함수 선언에 _fastcall을 붙일 경우 매개 변수 전달 과정에서 매개 변수 두 개를 stack에 넣는 대신 register에 넣어 전달한다. 물론 fastcall로 넘어가는 매개 변수 크기가 register 크기(즉, 32비트, 4바이트) 이하가 되어야 한다는 제약은 있다. x86 linux용 gcc compiler의 경우 3.4부터 attribute로 fastcall을 지원하기 시작했다. linux fastcall은 reigster %ecx, %edx를 사용해서 매개 변수 둘을 register에 담아 호출할 함수로 넘기는 방법을 사용한다.

fastcall_test.c
compile: gcc -S -D__i386__ -D__GCC__ fastcall_test.c

gcc version이 낮아 fastcall을 사용할 수 없는 경우 regparm 사용(version 2.7~3.4)
stack 대신에 매개 변수를 %eax, %edx, %ecx에 담아서 넘김(regparm에 넘긴 숫자는 register 사용 개수로 3을 초과할 수 없다)
가변 매개 변수를 사용할 경우에는 regparm을 지정해도 매개 변수를 모두 속성에 넣는 것을 주의

regparm_test.c

결론
  1. debugging을 제대로 하기 위해서는 stack에 대한 지식이 필요
  2. stack frame은 function을 호출할 때마다 생기는 stack의 기본 단위로 return address, auto variable, parameter와 저장해야 할 일부 register 값이 들어있음
  3. ABI는 Application Binary Interface로 application program과 OS / library / object file 사이에 일어나는 low level interface를 정의
  4. gdb와 같은 debugger로 auto variable을 확인하려면 호출 stack을 추적해야 한다. 이 때 사용하는 명령은 backtrace, down, up, frame과 같은 stack 관리 명령
  5. gdb와 같은 debugger로 stack frame information을 분석할 수 있고, 역 어셈블한 code를 사용해서 내부적인 동작 원리까지 파악할 수 있다
  6. gcc 신형 version의 경우에는 fastcall attribute를 이용해 register 두 개에 parameter를 넘길 수 있고, 구형 version의 경우 regparam attribute를 지정해 register 3개에 parameter를 넘길 수 있다
  7. 고급 언어 코드 내부 동작원리는 어셈블한 코드를 봐야 한다
참고
Binary Hacks: 해커가 전수하는 테크닉 100선, 사토루 다카바야시 외 공저, 진명조 역, ITC
리눅스 문제 분석과 해결, 박재호/이재영 역, 에이콘 출판사 2006년: 5장 - stack, 6장 GDB
호출 스택 소개: http://en.wikipedia.org/wiki/Call_stack
ABI 소개: http://en.wikipedia.org/wiki/Application_Binary_Interface
x86용 ABI: http://www.caldera.com/developers/devspecs/abi386-4.pdf
x86-64용 ABI: http://www.x86-64.org/documentation/abi.pdf
MS용 32비트 컴파일러 함수 호출 관례: http://support.microsoft.com/kb/100832

Comments