C and C++/C

[C언어] 환경변수를 출력하는 envp의 미스터리한 동작 발견. 이유가 대체 뭘까요? <extern char ** environ;> (1)

Razelo 2022. 4. 13. 00:15

야밤에 치킨을 먹고 쉬던 와중에 친구에게 연락이 왔다. 그리고 친구와 오랜만에 c 코드를 잠깐 살피게 되었다. 

간단한 코드인데 어떻게 제출할지가 살짝 애매해서 고민하고 있었다. 

방법을 찾으려고 이것 저것 건드려보면서 30분은 떠든것 같다. 

 

그러다가 굉장히 납득하기 어려운 동작을 하는 코드가 탄생했다. 아무리 생각해도 이해가 가지 않는 동작이라 조금만 더 살펴볼까 한다. 분명 우리가 모르는 무언가가 있을 것이라고 생각하고 이리저리 살펴봤는데도 그럼에도 불구하고 신기한 코드이다. 

 

이제부터 설명을 하도록 하겠다. 우선 우리의 목적은 간단하다. /usr/bin/env 는 환경변수를 모두 출력시키는 동작을 한다. 그리고 우리는 그 환경변수에 ENV1=value1, ENV2=value2 라는 path 또한 추가해서 출력시키게끔 하고 싶은 것이다. 그러니 기존 환경변수에 ENV1=value1 과 ENV2=value2라는 path 를 추가시키는 코드를 작성해야만 한다. 

참고로 리눅스에서 env 커맨드를 통해 환경변수를 모두 출력할 수 있다.

 

자 이제 궁금증이 생긴 코드를 살펴보자. 

/******************************************************************************

                            Online C Compiler.
                Code, Compile, Run and Debug C program online.
Write your code in this editor and press "Run" button to compile and execute it.

*******************************************************************************/
#include <unistd.h>
#include <stdio.h>

extern char** environ;
int main(void)
{
        char *argv[] = { "/usr/bin/env"};
        char *envp[] = {
                "ENV1=value1",
                "ENV2=value2",
                0
        };
        execve(argv[0], &argv[0], environ);

        return 0;
}

자 간단한 코드이다. environ 이라는 전역변수를 쓰고 있고 그냥 평벙한 main에 argv 를 통해 env 를 지정해주고 있으며 

envp 에서는 우리가 추가하고 싶었던 ENV1=value, ENV2=value2를 담고 있다. 

그리고 execve 를 통해 argv 를 전달함으로써 우리가 탄생시킬 child 프로세스는 env 를 실행할 것이다. 동시에 environ 이라는 전역변수를 전달받게 되는것이다. 참고로 environ 은 환경변수에 접근할 수 있는 전역변수이다. 

 

자 그렇다면 이제 결과를 예상해보자.

environ 만 그냥 던져줬으니 그냥 지금 현재의 환경변수들이 출력되지 않을까? 우리가 원하는 동작이 envp 에 명시된 path를 추가하는 것이었지만 추가하는 어느 동작이나, environ의 뒤에 이어붙인다거나? 혹은 심지어 다른 어떤 추가 동작은 없었다. 그냥 envp 는 선언된 것 뿐이다. 

 

하지만 결과는 정말 신기하게 나온다. 

 

다음과 같은 결과가 나온다. 

더보기

HOSTNAME=Check
LANGUAGE=en_US:en
PWD=/home
HOME=/home/runner5
LANG=en_US.UTF-8
GOROOT=/usr/local/go
TERM=xterm
DISPLAY=:1
SHLVL=1
PS1=#ogdbshell#
LC_ALL=en_US.UTF-8
PATH=/opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DEBIAN_FRONTEND=noninteractive
_=/script/tinit
ENV1=value1
ENV2=value2


...Program finished with exit code 0
Press ENTER to exit console.

자 맨 아래에 ENV1=value1 과 ENV2=value2가 추가된 것이 보이는가? 

왜 이게 추가가 됬을까? 이 문제만 가지고 별의별 가설을 세우고 친구와 11시가 되도록 이야기를 진행했는데 우리가 예상한 가설들은 모두 틀렸었다. 

 

논의한 가설들을 이야기해보겠다. 

첫째 친구의 가설: envp 는 사실 main의 input 으로써 사용될 수 있는 형식 중 하나이다. 즉 main(int argc, char* argv[], char* envp[]) 라고 자주 쓴다. 그러니 여기서 gcc 가 뭔가 main 으로 들어오는 인자로 선언된 envp 와 지역변수로 선언된 envp 를 살짝 혼동한게 아닐까? 비록 우리가 작성한 main 에서는 envp 를 명시해주진 않았지만 헷갈리지 않았을까? 

결론: 일단 이 가설은 틀렸다. 우리 코드에서 envp 를 단순히 char *test 라고 바꿔서 ENV1=value1, ENV2=value2 라고 해도 동일한 결과가 나온다. 즉 아래 사진처럼 해봤는데도 소용이 없었다. 물론 당연히 main 인자에 안넣어줬으니 당연히 안되리라 예측했지만 애초에 우리가 작성한 코드 동작이 이해가 안되서 gcc에서의 우리가 모르는 동작이 있지 않을까 해서 시도한 코드였다. 

두번째 나의 가설: 혹시 이 파일 자체를 컴파일할 적에 environ 이 메모리상에 할당된 위치 바로 뒤에 envp 가 allocate 되는 것은 아닐까? 라는 생각을 해보았다. 자 그리고 이러한 말도안되는(?) 가설의 가능성을 어느정도 확인하기 위해서 environ 과 envp 의 메모리 주소를 찍어보았다. 생각보다 차이가 얼마 안나는 것 같다는 친구의 말과 함께 나도 뭔가 이게 답이지 않을까라는 생각을 하게 되었다. 

 

자 그런데 이 두번째 가설은 친구는 시간이 늦어서 이제 가버리고 없고 나 혼자서 확인해보는 길이 남았다. 지금 글을 쓰면서도 아직 확인해보지 못했는데 이어서 글을 쓰면서 직접 모든 값들을 찍어보면서 확인해보는 방식을 진행해보겠다. 

 

자 간단한 printf 문을 적어놓고 주소값을 찍어보았다. 코드와 결과는 아래와 같다. 참고로 원하는 해답은 얻지 못했다. 두번째 가설도 틀렸다. 사실 말이 안되는 가설이었지만 생각할 수 있는게 저것 뿐이었다. 

/******************************************************************************

                            Online C Compiler.
                Code, Compile, Run and Debug C program online.
Write your code in this editor and press "Run" button to compile and execute it.

*******************************************************************************/
#include <unistd.h>
#include <stdio.h>

extern char** environ;
int main(void)
{
        char *argv[] = { "/usr/bin/env"};
        char *envp[] = {
                "ENV1=value1",
                "ENV2=value2",
                0
        };
        int i = 0; 
        while(environ[i] != NULL){
            printf("environ[%d]: %s , address: %d\n",i, environ[i], &environ[i]);
            i++; 
        }
        printf("envp: %d\n", &envp[0]); 
        printf("\n"); 
        execve(argv[0], &argv[0], environ);

        return 0;
}
더보기

environ[0]: HOSTNAME=Check , address: 1932536840
environ[1]: LANGUAGE=en_US:en , address: 1932536848
environ[2]: PWD=/home , address: 1932536856
environ[3]: HOME=/home/runner27 , address: 1932536864
environ[4]: LANG=en_US.UTF-8 , address: 1932536872
environ[5]: GOROOT=/usr/local/go , address: 1932536880
environ[6]: TERM=xterm , address: 1932536888
environ[7]: DISPLAY=:1 , address: 1932536896
environ[8]: SHLVL=1 , address: 1932536904
environ[9]: PS1=#ogdbshell# , address: 1932536912
environ[10]: LC_ALL=en_US.UTF-8 , address: 1932536920
environ[11]: PATH=/opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin , address: 1932536928
environ[12]: DEBIAN_FRONTEND=noninteractive , address: 1932536936
environ[13]: _=/script/tinit , address: 1932536944
envp: 1932536544

HOSTNAME=Check
LANGUAGE=en_US:en
PWD=/home
HOME=/home/runner27
LANG=en_US.UTF-8
GOROOT=/usr/local/go
TERM=xterm
DISPLAY=:1
SHLVL=1
PS1=#ogdbshell#
LC_ALL=en_US.UTF-8
PATH=/opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DEBIAN_FRONTEND=noninteractive
_=/script/tinit
ENV1=value1
ENV2=value2


...Program finished with exit code 0
Press ENTER to exit console.

 

도대체 이유가 뭘까... 두번째 가설도 틀렸다. envp 의 메모리 주소가 찍힌걸 보면 environ 의 요소들의 메모리 주소를 찍었을경우에서 인접한 메모리위치에 존재하지 않는다. 이전에 메모리 위치를 어렴풋이 찍어놓고 차이가 얼마안난다고 이야기했었는데 가만 생각해보면 메모리 단위가 상당히 작다는걸 무시하고 그저 가능한 가설이기에 무작정 해본것 같다. 

 

이제 이유를 찾을 방법이 뭐가 있을까. 

 

실마리를 찾은것 같다. 아래 코드를 보자. 

 

/******************************************************************************

                            Online C Compiler.
                Code, Compile, Run and Debug C program online.
Write your code in this editor and press "Run" button to compile and execute it.

*******************************************************************************/
#include <unistd.h>
#include <stdio.h>

extern char** environ;
int main(int argc, char* argv[], char* envp[])
{
        *argv = "/usr/bin/env";
        *envp ="ENV1=value1"; 
        printf("envp: %d\n\n", &envp);

        execve(argv[0], &argv[0], environ);

        return 0;
}
더보기

envp: 2141714120

ENV1=value1
LANGUAGE=en_US:en
PWD=/home
HOME=/home/runner30
LANG=en_US.UTF-8
GOROOT=/usr/local/go
TERM=xterm
DISPLAY=:1
SHLVL=1
PS1=#ogdbshell#
LC_ALL=en_US.UTF-8
PATH=/opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DEBIAN_FRONTEND=noninteractive
_=/script/tinit


...Program finished with exit code 0
Press ENTER to exit console.

결과를 보면 맨 처음 라인에 ENV1=value1이라고 추가된 것을 볼 수 있을 것이다. main 이 인자를 받는 것으로 살짝 바꾸었는데 이때 세번째 인자에 환경변수에 접근가능한 envp 라는 친구를 추가해주었다. 

그리고 *envp 를 통해서 메인에서 envp 의 값을 ENV1=value1 이라고 정해주었다. 그리고 그 결과가 위와 같이 나온거다.

 

즉 여기서 알 수 있는 사실은 다음과 같다. 

1. execve 를 통해서 env 를 출력시키면 environ 에 있는 내용뿐만아니라 envp 내용까지 출력시킨다는 것이다. 

2. environ 의 내용을 출력하는것보다 envp 의 내용을 출력하는게 선행된다. 

 

 

위와 같은 사실 덕분에 이제 이해가 되었다. 자 그런데 1번은 결과로 확인할 수 있으니 ok고, 2번도 결과로 확인했으니 ok 다. 

 

이로써 위 두가지 사실을 알아낼 수 있었다. 

 

하지만 마지막까지 이해가 가지 않은 사실이 있다. 

envp 를 컴파일러 내에서 자동으로 찾아서 인식하는걸까? 

main 인자에 선언된 envp 의 네임 자체를 찾는건가? 

envp 가 지역변수에 할당된건데 어떻게 그게 path variable을 의미하는 환경변수로 잡힐 수 있는건지? 

잡힐 수 있다면 첫번째,두번째 궁금증이 맞다는 이야기인데? 

 

시간이 늦어서 여기까지 확인해보았다. 

 

다만 확실한건 지식이 부족해서다. 이런 류의 문제나 궁금증은 대개 내가 모르는 하나의 중요한 사실이 있을때 발생한다. 왜 돌아가는지도 모르고 직접 찍어서 확인해봐도 찜찜하다는건 뭔가 아주 중요한 걸 빠뜨린거다. 뭘까 그게. 

 

오늘은 여기까지 보는게 낫겠다. 너무 늦었다. 혹시라도 이글을 보시는 분이 계시다면 댓글로 도움을 주기 바란다. 


2022-04-13 (수)

 

자 어젯밤에 이것저것 만져보면서 고민을 했었고 너무 시간이 늦어서 일단 잤다. 푹자고 일어나서 다시 생각해보았다. 

그리고 해결했다... 아근데 100퍼센트 해결이 아니라 70퍼센트 해결이다. 약간 찜찜한게 남아있는 상태로 해결을 했다. 하지만 이 상태에서 뭔가 좀 더 파보면 분명 뭔가 더 나올거라는 예감이 든다. 일단 진행상황을 설명하겠다. (c언어는가끔씩 잊고 지내다가 신기한걸 발견하면 다시 돌아와서 이것저것 복기하면서 다시 만지작거리는 재미가 있다. 즉 깊이가 있다. )

새로 작성한 코드를 보여주겠다. 어제 작성한 포스팅에서 처음에 첨부한 코드와 아주 유사하다. 목적도 동일하다. 그리고 이 코드에서는 환경변수 뒤에 우리가 추가하지도 않았던 환경변수가 추가되는 일은 없다. 그저 원래 존재했던 환경변수가 출력되서 확인될 뿐이다. 

/******************************************************************************

                            Online C Compiler.
                Code, Compile, Run and Debug C program online.
Write your code in this editor and press "Run" button to compile and execute it.

*******************************************************************************/

#include <unistd.h>
#include <stdio.h>

extern char** environ;
int main(void)
{
        char *v[2];
        v[0] = "/usr/bin/env";
        v[1] = NULL;
        char *envp[] = {
                "ENV1=value1",
                "ENV2=value2",
                0
        };
        execve(v[0],v, environ);

        return 0;
}
더보기

HOSTNAME=Check
LANGUAGE=en_US:en
PWD=/home
HOME=/
LANG=en_US.UTF-8
GOROOT=/usr/local/go
TERM=xterm
DISPLAY=:1
SHLVL=1
PS1=#ogdbshell#
LC_ALL=en_US.UTF-8
PATH=/opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DEBIAN_FRONTEND=noninteractive
_=/script/tinit


...Program finished with exit code 0
Press ENTER to exit console.

 

자 뭔가 달라졌다. 우선 이전에 내가 소개했던 처음 코드 즉 이상한 동작을 하는코드는 환경변수에 envp 에 있는 내용을 붙이거나 혹은 추가해주거나 하는 동작은 없었음에도 ENV1=value1, ENV2=value2 라는 환경변수가 떡하니 추가되서 출력됬었다. 하지만 위 코드에서 그런 일은 없다. 그저 기존 환경변수만 잘 출력된다. 

 

무슨 차이가 있을까? 

우선 잘보면 /usr/bin/env 를 어떻게 가져가느냐가 달라졌다. 

이전 코드에서는 argv 라는 char 배열을 선언하고 거기에 "/usr/bin/env" 라고 넣었었다. 

하지만 이번 코드에서는 char v 배열을 2사이즈로 선언하고 0번 요소에는 /usr/bin/env 를 넣고 

1번 요소에는 끝을 나타내는 NULL 을 넣었다. 

 

그리고 나니 추가가 이상한 동작은 사라졌다. 

즉 해결은 되었다. 

 

다만 이제 이 둘 코드사이에 뭔 차이가 있는지 생각해볼 일만 남았다. 

 

자 문자열 배열 어딘가에 해답이 있을 것같다.  뭘까. 자 이제 답에 근접했다. 아래 코드를 보자. 어제자 기존 코드에서 아주 살짝의 변형을 주고 예상치 못한 동작을 없앨 수 있었다. 코드를 보자. 

/******************************************************************************

                            Online C Compiler.
                Code, Compile, Run and Debug C program online.
Write your code in this editor and press "Run" button to compile and execute it.

*******************************************************************************/
#include <unistd.h>
#include <stdio.h>

extern char** environ;
int main(void)
{
        char *argv[2] = { "/usr/bin/env", NULL};
        
        char *envp[] = {
                "ENV1=value1",
                "ENV2=value2",
                0
        };
        execve(argv[0], &argv[0], environ);

        return 0;
}
더보기

HOSTNAME=Check
LANGUAGE=en_US:en
PWD=/home
HOME=/home/runner21
LANG=en_US.UTF-8
GOROOT=/usr/local/go
TERM=xterm
DISPLAY=:1
SHLVL=1
PS1=#ogdbshell#
LC_ALL=en_US.UTF-8
PATH=/opt/swift/swift-5.0-RELEASE-ubuntu14.04/usr/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DEBIAN_FRONTEND=noninteractive
_=/script/tinit


...Program finished with exit code 0
Press ENTER to exit console.

 보면 알겠지만 char * argv[] 가 이전에는 배열 사이즈가 선언되어있지 않았다. 그런데 이번에는

 배열 사이즈를 2로 주고 두번째 요소를 NULL 로 잡아줬다. 그러고 나니 예상치 못한 동작이 사라졌다. 

지금 출력된 결과에 ENV1=value1, ENV2=value2 라는게 없는게 보이죠? 이제 근접한것 같다. 

 

문자열 배열 초기화와 관련된 어딘가에 내가 모르는 뭔가가 있다. 까먹었거나 아예 처음부터 몰랐던 내용. 

 

이후에 다시 돌아와서 해결해보도록 하곘다. 

 

 

 

반응형