시각화 말고 좀 더 자세하고 보고 싶다면
Visual Studio 디버거를 쓰는 게 좋습니다.
Visual Studio를 설치할 수 없는 환경이면
온라인 GDB라고 있습니다.
https://onlinegdb.com/Z0_UN7yWv
이걸 이용해서 구체적인 변수 값들을 추적해보세요.
gcc 환경이면 gdb도 설치되어 있겠지만, 처음 쓰긴 어렵습니다. 온라인 GDB를 쓰는 게 쉽습니다.
알려주신 디버깅 사이트,코드 시각화 사이트와 제가 추가로 조사한 바에 따라서
결국 va_list는 구조체 배열 포인터이고 va_list 자체는 포인터니까 출력해도 값(주소)은 똑같은데 va_list가 가르키는 구조체 안에 있는 값들이 바뀌면서 다음 인수를 가르키고, 또 초기화되는 동작을 한다고 이해했습니다. 이렇게 이해하면 될까요?
F11로 단계별로 실행해보면 됩니다. 반복문을 시작하기 전에 va_start(ap, args)까지 실행했을 때...
va_list ap;로 선언했었는데, 디버거 로컬 창을 보면 ap 오른쪽에 형식은 char *이라는 것을 알 수 있습니다.
컴파일러 구현체마다 구현은 다를 수 있지만, 보통은 다음과 같습니다.
typedef char* va_list;typedef로 va_list라는 타입을 만든 것입니다. char *입니다.
ap의 주소는 ef848로 끝납니다.
F11로 계속 실행하면
i = 0에서 첫 번째 루프를 실행하고, 화면에 10을 출력하고 ap는 ef850으로 주소가 변경되었습니다.
ap는 전체 인수가 있는 시작 주소를 가리키고 있었고, 첫 번째 int 인수를 읽어서 출력한만큼 위치가 이동한 것입니다.
그림 66-2에 해당 과정이 설명되어 있습니다.
첫 번째 반복문이 끝났을 때 명령창에는 다음과 같이 출력된 상태입니다.
10 00000093941EF850
va_arg로 값을 읽고 ap 위치가 이동하기 전에 ap 주소를 출력했다면 읽기 전의 주소를 출력했을 겁니다. 지금은 읽은 이후에 다음 주소를 출력하고 있습니다. 이게 첫 번째 질문에서 ap 위치를 오해한 이유일 겁니다.
for (int i = 0; i < args; i++)
{
printf("%p ", ap); // va_arg에서 분명히 이동을 했을텐데 왜, 그대로인가?
printf("%d ", va_arg(ap, int));
printf("%p ", ap); // va_arg에서 분명히 이동을 했을텐데 왜, 그대로인가?
}
이렇게 두 가지로 출력했다면 before, after의 ap 주소를 확실하게 알 수 있을 겁니다.
노란색 화살표 위치는 두 번째 반복문 실행하기 전입니다.
이 상태에서 ap의 주소는 ef850이고, ap가 가리키는 곳에는 값 20이 들어있습니다.
메모리에 접근할 때는 char*로 접근하고 있습니다. 그래서 int로 변환해서 값을 가져와야 합니다.
루프 안을 실행하고 20을 출력하고, 주소도 출력했다면 루프 마지막에서 ap는 이미 다음 주소로 이동한 상태입니다.
ap 주소는 ef858이고, 값은 30이 들어있습니다.
i = 1인데 이는 아직 2번째 반복문 실행 중이라서 그렇습니다.(for의 마지막 줄)
Visual Studio 설치와 실행에 시간이 걸리더라도 가능하면 한 번 설치해서 연습해보기 바랍니다.
현재 제한적인 환경이라 code-server라는 visual studio의 웹버전을 사용하고 있습니다.
이 환경에서는 아직 gcc로 세팅하는게 최선이라, 여건이 될 때 visual studio를 설치하여 꼭 디버깅 해보겠습니다.
현재는 답변해주신 것과 같은 코드여도 결과값이 다른 것으로 보입니다.
저는 디버깅을 하면 ap가 배열이라고 뜹니다(이는 gdb 온라인 디버깅, 코드 시각화사이트에서도 마찬가지입니다.)
그리고, 설명해주신 것처럼 va_arg전에 주소값을 출력해도 똑같은 값이 출력됩니다.
이는 결국 언급하신 것처럼 ap에 대한 컴파일러 구현체마다 구현은 다를 수 있어서 생긴 문제인가요?
주소값 출력 모습입니다
디버깅시, ap의 모습입니다.
디버거가 표현하는 방식의 차이일 뿐입니다.
내부 구현이나 디테일에 집착하는 우는 범하지 않길 바랍니다.
스택 오버플로에도 질문은 있지만, 이는 ABI 포맷에 대한 얘기이고, 운영체제나 컴파일러를 직접 구현할 사람이 아니면 필요한 내용이 아닙니다.
https://stackoverflow.com/questions/4958384/what-is-the-format-of-the-x86-64-va-list-structure
GCC/Clang 등의 오픈 소스 소스 코드를 인터넷에서 쉽게 받을 수 있지만, 동작을 이해하고, 현대 프로세서 구조와 ABI까지 이해하려면 몇 년의 시간을 할당해야 합니다. 이 정도 레벨이 되면 보통은 '프로그래밍 연구실'에 석사 과정으로 들어가서 정적 분석에 대한 구현 논문을 쓰는 상황일 것이고, ABI를 이해하고 개선안을 내놓을 정도면 박사 논문을 쓰고 있을 겁니다.
자동차 운전을 한다고 해서 자동차 엔진의 원리를 이해하는 사람은 없으며 운전을 30년 동안 했다고 해서 엔진을 이해하는 사람은 없습니다.
va_list는 array가 맞고, Visual Studio는 ap 메모리 값으로 보여주고 있습니다. C 언어 표준에는 array라고 되어 있습니다.
단, C 언어는 내부적으로 배열을 모두 포인터로 처리합니다.
포럼 상단 자주 묻는 질문이 있습니다.
https://dojang.io/mod/forum/discuss.php?d=101
함수 인자에 char*냐 char[]이냐는 오래된 논쟁입니다.
https://dojang.io/mod/forum/discuss.php?d=599&parent=1352
내부적으로는 typdef char* va_list이고, 디버거가 보여주는 방식의 차이로 보입니다.
stdarg.h에 선언되어 있는 데 보통은
va_list를 __builtin_va_list로 선언하고 있고, 컴파일러 내부 구현의 문제이고, 결국 ABI도 연결됩니다.
https://stackoverflow.com/questions/49733154/how-is-builtin-va-list-implemented
여기서도 같은 논지로 설명하고 있습니다.
저도 ABI는 모릅니다. ABI를 아는 현업 개발자는 거의 없을 겁니다. 컴파일러 연구자나 운영체제쪽이 아니면 알 필요가 없습니다.
hello.exe 실행 파일을 실행하는 과정도 loader가 필요하고, 역시나 연구자의 영역입니다. JAVA 프로그램을 실행할 때도 JVM Loader가 필요합니다. 역시나 연구자의 영역입니다. 아무도 Loader의 기본 동작, bootstrapping 과정을 설명하지 못합니다. 그럼에도 개발자이고, CTO이고, 코딩 잘 하고 있습니다.
깊이에 천착할 필요가 없습니다. 국내에서 컴파일러 연구자는 매우 드물기 때문에 이 길을 선택한다면 아예 해외로 나가셔야 할 겁니다.
영어 알파벳 공부하면 알파벳을 외우지, 알파벳의 기원을 찾지는 않습니다. 알파벳에서는 숫자는 I, II, III, IV, V인데 왜 갑자기 세계는 아라비아 숫자인 1, 2, 3, 4, 5를 쓰게 되었는가를 탐구하지 않습니다. 이건 역사학자나 언어학자의 몫입니다. 지금까지 숫자 잘 쓰고 있잖아요? 숫자의 영어는 number인데 왜 약어는 no.라고 쓰는가? 의문을 품지 않고 잘 살았잖아요? 이건 또 왜 라틴어 numero의 약어를 쓰는가? 언어는 왜 이 따위로 생겼나... 그런 건 학자들의 몫입니다.
우리는 사용만 잘 하면 됩니다. 학문의 길이 목적이면 깊이 파고 들어야 합니다.
ps. 종종 지엽적인 것을 파고들면서 깊이 공부한다고 하는 입문자들의 질문이 보여서요. 변수는 왜 변수라고 하나요? 라고 묻는 경우도 있어서요.
저는 무지하기에 제가 모르는 것이 지엽적인 것인지 깊이 공부하는 것인지 모르기에 이런 질문을 드린 것 같습니다.
말씀하신 내용은 비단 이번뿐이 아니라, 프로그래밍 공부를 하면서 어느정도 기준선으로 삼겠습니다.
자세한 예시를 포함한 친절한 설명 정말 감사합니다.
답변하는데 쓰신 시간과 노력이 헛수고가 되지 않도록 열심히 하겠습니다.