Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 2개 있습니다.)
Windows: 274. Windows 7부터 도입한 conhost.exe
; https://www.sysnet.pe.kr/2/0/13832

Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우)
; https://www.sysnet.pe.kr/2/0/13834




C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우)

이번 글은 Windows 7과 2012 R2 환경에서 테스트했지만, Windows 10의 새로운 터미널 방식이 아닌, 레거시(?) 환경에 모두 적용될 수 있습니다.




Console App은 비록 (직접 코드 내에서) 윈도우 자원을 생성하지는 않지만, 결국 그 화면은 윈도우입니다.

console_win7_1.png

실제로 Console App에서 저 윈도우의 핸들 값을 Win32 API를 이용해 구할 수도 있는데요,

GetConsoleWindow function
; https://learn.microsoft.com/en-us/windows/console/getconsolewindow

닷넷의 경우 p/invoke로 다음과 같이 코딩할 수 있습니다.

using System.Runtime.InteropServices;

internal class Program
{
    [DllImport("kernel32.dll")]
    static extern uint GetCurrentThreadId();

    [DllImport("kernel32.dll")]
    static extern IntPtr GetConsoleWindow();

    static void Main(string[] args)
    {
        IntPtr hWnd = GetConsoleWindow();
        int pid = Environment.ProcessId;
        uint tid = GetCurrentThreadId();

        Console.WriteLine($"Process ID: {pid} (0x{pid:x})");
        Console.WriteLine($"Thread ID: {tid} (0x{tid:x})");
        Console.WriteLine($"HWND: {hWnd} (0x{hWnd:x})");

        Console.WriteLine($"{Environment.NewLine}Press any key to exit...");
        Console.ReadLine();
    }
}

빌드 후, 윈도우 탐색기에서 EXE 파일을 직접 더블 클릭해 실행하면 아래와 같은 유형의 출력 결과가 나올 텐데요,

Process ID: 1460 (0x5b4)
Thread ID: 3492 (0xda4)
HWND: 1573526 (0x180296)

Press any key to exit...

이 상태에서 Spy++을 실행해 윈도우와 그것을 생성한 Process ID, Thread ID를 살펴보면,

console_win7_2.png

위와 같이 "Console" 창이 곧 ConsoleApp3.exe 프로세스에서 생성한 윈도우임을 확인할 수 있습니다.




Console 프로그램(위의 경우 ConsoleApp3.exe)을 만약 탐색기가 아닌, 미리 실행해 둔 cmd.exe 명령행 창에서 실행하면 어떻게 될까요?

엄밀히 말해서 cmd.exe도, 우리가 만든 ConsoleApp3.exe도 모두 "Windows CUI" 유형에 속합니다.

c:\temp> dumpbin /HEADERS c:\windows\system32\cmd.exe

...[생략]...
OPTIONAL HEADER VALUES
             20B magic # (PE32+)
           ...[생략]...
           5360A checksum
               3 subsystem (Windows CUI)
...[생략]...

즉, cmd.exe를 실행하면 위에서 살펴 본 ConsoleApp3.exe 실행과 동일하게 그 프로세스 내에 Console을 위한 윈도우가 할당됩니다. 그리고, 바로 그 cmd.exe 창 내에서 ConsoleApp3.exe를 실행하면,

console_win7_3.png

[Windows 7에서 실행하는 경우]

csrss.exe
  ㄴ conhost.exe

cmd.exe (PID: 416(0x1a0)) 
  |- 윈도우 - 0x380278 (ClassName: ConsoleWindowClass)
  ㄴ ConsoleApp3.exe (PID: 3532(0xdcc))

ConsoleApp3 코드에서 실행한 GetConsoleWindow 함수는 부모 프로세스(위의 경우 cmd.exe)가 생성한 Console 창의 윈도우 핸들(0x380278)을 반환합니다. 또한 저 속성 창을 보면 윈도우 위치와 크기를 나타내는 Rectangle 정보가 현재 Console Window의 위치와 일치하는 것을 확인할 수 있습니다.

그런데 과연 ConsoleApp3.exe는 어떻게 부모 윈도우의 콘솔 윈도우와 I/O 연동을 할 수 있을까요? 간단합니다. cmd.exe는 표준 입력, 표준 출력, 표준 에러 출력을 ConsoleApp3.exe로 상속해 주기 때문입니다.

정말 그런지 직접 확인을 해볼까요? ^^ 우선, 콘솔 앱은 자신에게 할당된 표준 I/O에 대한 핸들 값을 이렇게 구할 수 있습니다.

const int STD_INPUT_HANDLE = -10;
const int STD_OUTPUT_HANDLE = -11;
const int STD_ERROR_HANDLE = -12;

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetStdHandle(int nStdHandle);

private static void PrintStandardHandle()
{
    PrintHandle("StandardOutputHandle", STD_INPUT_HANDLE);
    PrintHandle("StandardOutputHandle", STD_OUTPUT_HANDLE);
    PrintHandle("StandardOutputHandle", STD_ERROR_HANDLE);
}

static void PrintHandle(string title, int handleType)
{
    IntPtr pHandle = GetStdHandle(handleType);
    Console.WriteLine($"{title}: {pHandle} (0x{pHandle:x})");   
}

(아래에서 테스트할 코드로 인해 Windows 7이 아닌) Windows 2012 R2 환경에서 코드를 실행하니 이런 결과가 나옵니다.

StandardInputHandle : 28 (0x1c)
StandardOutputHandle: 32 (0x20)
StandardErrortHandle: 36 (0x24)

이제 저 핸들이 상속 유형인지 알아내면 되는데요, 이를 위해 예전에 만들어 둔 윈도우 핸들을 열거하는 코드를 사용해,

C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/12195

이렇게 코드를 추가할 수 있습니다.

// Install-Package KernelStructOffset

int processId = Environment.ProcessId;

using (ProcessHandleInfo phi = new ProcessHandleInfo(processId)) // 이 코드에서 사용하는 NtQueryInformationProcess API가 Windows 8/2012 R2 이상에서만 동작
{
    for (int i = 0; i < phi.HandleCount; i++)
    {
        _PROCESS_HANDLE_TABLE_ENTRY_INFO phe = phi[i];

        // PROCESS_HANDLE_TABLE_ENTRY_INFO - NtDoc
        // ; https://ntdoc.m417z.com/process_handle_table_entry_info
        // HandleAttributes
        // A bit mask containing attributes of the handle/object:
        // 
        // OBJ_PROTECT_CLOSE - the handle is protected from closing.
        // OBJ_INHERIT - the handle is inheritable.
        // OBJ_PERMANENT - object has permanent lifetime.
        // OBJ_EXCLUSIVE - the handle/object is exclusive and prevents other handles from being open to the object.

        if ((phe.HandleAttributes & OBJ_INHERIT) != OBJ_INHERIT)
        {
            continue;
        }

        string objName = phe.GetName(processId, out string handleTypeName);
        if (string.IsNullOrEmpty(handleTypeName) == true)
        {
            continue;
        }

        if (string.IsNullOrEmpty(objName) == true)
        {
            continue;
        }

        Console.WriteLine($"{phe.HandleValue} {handleTypeName} {objName}");
    }
}

위의 코드는 현재 프로세스의 모든 핸들을 열거한 후 OBJ_INHERIT 플래그가 설정된 것만 출력합니다. 그래서 이걸 실행하면 이제 다음과 같은 출력을 얻을 수 있습니다.

StandardInputHandle : 28 (0x1c)
StandardOutputHandle: 32 (0x20)
StandardErrortHandle: 36 (0x24)

28 File \Device\ConDrv // == 상속받은 핸들만 출력
32 File \Device\ConDrv
36 File \Device\ConDrv

보는 바와 같이 동일한 핸들에 대해, 즉, 표준 입력, 표준 출력, 표준 에러 출력에 대해 상속 플래그가 존재합니다. 그렇다면, 이 프로세스에서 자식으로 또다시 Console Application을 실행하면 어떻게 될까요?

역시나 그 자식 프로세스도 부모로부터 표준 I/O 및 에러 출력 핸들을 상속받게 되므로 위와 동일한 결과가 나올 것입니다. 그리고 이 테스트를 실제로 다음과 같이 할 수 있습니다.

using KernelStructOffset;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace ConsoleApp2;

internal class Program
{
    const int STD_INPUT_HANDLE = -10;
    const int STD_OUTPUT_HANDLE = -11;
    const int STD_ERROR_HANDLE = -12;

    public const uint OBJ_INHERIT = 0x00000002;

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetStdHandle(int nStdHandle);

    static void Main(string[] args)
    {
        PrintStandardHandle();
        Console.WriteLine();

        int processId = Environment.ProcessId;

        using (ProcessHandleInfo phi = new ProcessHandleInfo(processId))
        {
            for (int i = 0; i < phi.HandleCount; i++)
            {
                _PROCESS_HANDLE_TABLE_ENTRY_INFO phe = phi[i];

                if ((phe.HandleAttributes & OBJ_INHERIT) != OBJ_INHERIT)
                {
                    continue;
                }

                string objName = phe.GetName(processId, out string handleTypeName);
                if (string.IsNullOrEmpty(handleTypeName) == true)
                {
                    continue;
                }

                if (string.IsNullOrEmpty(objName) == true)
                {
                    continue;
                }

                Console.WriteLine($"{phe.HandleValue} {handleTypeName} {objName}");
            }
        }

        if (args.Length > 0 && args[0] == "/child")
        {
            return;
        }

        Console.WriteLine("----------------------");

        ProcessStartInfo psi = new ProcessStartInfo();
        psi.FileName = Process.GetCurrentProcess().ProcessName;
        psi.Arguments = "/child";
        psi.UseShellExecute = false;
        Process.Start(psi)?.WaitForExit();

        Console.WriteLine($"{Environment.NewLine}Press any key to exit...");
        Console.ReadLine();
    }

    private static void PrintStandardHandle()
    {
        PrintHandle("StandardInputHandle ", STD_INPUT_HANDLE);
        PrintHandle("StandardOutputHandle", STD_OUTPUT_HANDLE);
        PrintHandle("StandardErrortHandle", STD_ERROR_HANDLE);
    }

    static void PrintHandle(string title, int handleType)
    {
        IntPtr pHandle = GetStdHandle(handleType);
        Console.WriteLine($"{title}: {pHandle} (0x{pHandle:x})");   
    }
}

위의 코드를 실행하면, 이렇게 출력이 나옵니다.

StandardOutputHandle: 28 (0x1c)     // == 부모 프로세스의 출력
StandardOutputHandle: 32 (0x20)
StandardOutputHandle: 36 (0x24)

28 File \Device\ConDrv
32 File \Device\ConDrv
36 File \Device\ConDrv
----------------------
StandardOutputHandle: 28 (0x1c)     // == 자식 프로세스의 출력
StandardOutputHandle: 32 (0x20)
StandardOutputHandle: 36 (0x24)

28 File \Device\ConDrv
32 File \Device\ConDrv
36 File \Device\ConDrv

동일한 핸들이 상속되면서 콘솔 타입의 프로그램들은 같은 콘솔 창의 표준 입출력을 공유하는 것입니다. 그렇기 때문에, 원한다면 해당 핸들을 상속하지 않음으로써, 설령 자식 프로세스가 콘솔 타입이라고 해도 화면 출력을 막는 것이 가능합니다. 위의 예제에서는 이러한 테스트를 다음과 같이 할 수 있습니다.

public const uint HANDLE_FLAG_INHERIT = 0x01;

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetHandleInformation(IntPtr hObject, uint dwMask, uint dwFlags);

// ...[생략]...

Console.WriteLine("----------------------");

SetHandleInformation(GetStdHandle(STD_INPUT_HANDLE), HANDLE_FLAG_INHERIT, 0); // 상속 금지
SetHandleInformation(GetStdHandle(STD_OUTPUT_HANDLE), HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(GetStdHandle(STD_ERROR_HANDLE), HANDLE_FLAG_INHERIT, 0);

// ...[생략]...

다시 실행하면, 자식 프로세스의 출력 정보가 나오지 않게 됩니다.

StandardInputHandle : 28 (0x1c)
StandardOutputHandle: 32 (0x20)
StandardErrortHandle: 36 (0x24)

28 File \Device\ConDrv
32 File \Device\ConDrv
36 File \Device\ConDrv
----------------------  // 자식 프로세스의 출력이 없음

참고로, Windows Forms/WPF 유형이라면 어떨까요? 이런 프로그램들은 모듈의 헤더에 Windows GUI라고 이미 설정돼 있어,

c:\temp> dumpbin /headers WinFormsApp1.exe
...[생략]...

OPTIONAL HEADER VALUES
...[생략]...
               2 subsystem (Windows GUI)
...[생략]...

윈도우 운영체제는 해당 앱들의 경우 실행 시 Console 연결을 하지 않습니다. 그래서 표준 입/출력, 에러 출력도 없고,

// Windows Forms에서 실행한 경우

IntPtr h1 = GetStdHandle(STD_INPUT_HANDLE); // h1 == IntPtr.Zero
IntPtr h2 = GetStdHandle(STD_OUTPUT_HANDLE); // h2 == IntPtr.Zero
IntPtr h3 = GetStdHandle(STD_ERROR_HANDLE); // h3 == IntPtr.Zero

당연히 그 핸들의 상속도 없습니다. 그러니까, GUI 유형의 앱들은 스스로 윈도우를 생성하고 그려야만 하는 것입니다. 대신 WPF/Windows Forms 유형의 앱들도 AllocConsole API를 이용하면 별도의 콘솔 윈도우를 연결할 수는 있습니다.

AllocConsole function
; https://learn.microsoft.com/en-us/windows/console/allocconsole

위의 API 문서에도 이와 관련된 설명이 나옵니다.

This function is primarily used by a graphical user interface (GUI) application to create a console window. GUI applications are initialized without a console. Console applications are initialized with a console, unless they are created as detached processes (by calling the CreateProcess function with the DETACHED_PROCESS flag).





콘솔의 이런 특징 때문에 재미있는, 때로는 약간 혼란스러운 동작을 보게 됩니다. 일례로, Visual Studio에서 Console App을 실행할 때가 그런 경우입니다.

Visual Studio의 Ctrl + F5 실행 동작
; https://www.sysnet.pe.kr/2/0/10848

예전에는 위의 글처럼, F5와 Ctrl+F5 간의 동작이 달랐는데요, 근래의 Visual Studio 2022에서는 2가지 실행 모두 동일하게 닫히지 않는 유형으로 바뀌었습니다. 그래도 약간의 차이가 있다면, Ctrl+F5 실행 시에는 exit code만 출력해 주는 반면,

console_win7_4.png

Hello World!

C:\temp\ConsoleApplication1\x64\Debug\ConsoleApplication1.exe (process 52988) exited with code 0.
Press any key to close this window . . .

F5 디버깅으로 종료한 경우에는 옵션을 통해 바꿀 수 있다는 문구까지 추가합니다.

Hello World!

C:\temp\ConsoleApplication1\x64\Debug\ConsoleApplication1.exe (process 53820) exited with code 0.
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .

이 외에도, 눈에 띄지 않게 달라진 점이 하나 있는데요, 예전에는 cmd.exe를 부모로 콘솔 응용 프로그램을 실행했지만,

"C:\WINDOWS\system32\cmd.exe" /c ""C:\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe"  & pause""

cmd.exe
   |- conhost.exe (Windows 2012 R2 이후의 환경)
   ㄴ ConsoleApp1.exe

지금의 Visual Studio 2022는 (제가 테스트한 Windows 11 환경에서) 그 부모를 VsDebugConsole.exe 전용으로 바꾼다는 점입니다.

C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Platform\Debugger\VsDebugConsole.exe

VsDebugConsole.exe
   |- conhost.exe (Windows 2012 R2 이후의 환경)
   ㄴ ConsoleApp1.exe

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




전에도 conhost에 대해 설명했지만, 이쯤에서 conhost와 우리가 만든 ConsoleApp1의 관계를 정리해 볼까요? ^^ 이에 대해서는 아래의 글에서 잘 설명하고 있습니다.

Windows Command-Line: Inside the Windows Console
; https://devblogs.microsoft.com/commandline/windows-command-line-inside-the-windows-console/

On Windows, however, things work differently: Windows users never launch the Console (conhost.exe) itself: Users launch Command-Line shells and apps, not the Console itself!


그러니까, 엄밀한 구분의 의미로 "Console"은 conhost.exe를 의미하고 콘솔 윈도우의 관리 및 입출력은 그 프로세스가 합니다. 사용자가 실행한 CUI 응용 프로그램은 사실 Console 자체는 아닌 것입니다. 이에 대해 또다시 설명이 이어지는데요,

Because users run Cmd.exe or PowerShell.exe and see a Console window appear, they labor under the common misunderstanding that Cmd and PowerShell are, themselves, "Consoles" … they’re not! Cmd.exe and PowerShell.exe are "headless" Command-Line applications that need to be attached to a Console (conhost.exe) instance from which they receive user input and to which they emit text output to be displayed to the user.

Also, many people say "Command-Line apps run in the Console". This is misleading and contributes additional confusion about how Consoles and Command-Line apps actually work!

...[생략]... "Command-Line tools/apps run connected to a Console"


즉, cmd.exe나 powershell.exe 및 기타 우리가 만든 CUI 프로그램들은 "Headless" 응용 프로그램입니다. 단지 conhost.exe가 관리하는 Console에 (그 내부에서 실행되는 것이 아니라) 연결돼 입/출력을 할 수 있게 되는 프로그램으로 동작하는 것입니다.

이에 비춰서 Windows GUI 프로그램도 "Headless" 응용 프로그램으로 볼 수 있습니다. 단지 그것은 윈도우 운영체제가 conhost.exe의 Console로 연결하는 작업을 하지 않는 차이가 있는 것입니다.




개인적으로 해석이 안 되는 결과가 있는데요, 위의 내용에서 Windows Server 2012에서 실행했을 때 표준 입출력 핸들이 이런 식으로 출력된다고 했는데요,

// Windows Server 2012 이상에서 실행한 경우

StandardInputHandle : 28 (0x1c)
StandardOutputHandle: 32 (0x20)
StandardErrortHandle: 36 (0x24)

저걸 Windows 7 환경에서 실행하면 이런 결과가 나옵니다.

// Windows 7 이하에서 실행한 경우

StandardInputHandle : 3 (0x3)
StandardOutputHandle: 7 (0x7)
StandardErrortHandle: 11 (0xb)

핸들 값이 뭔가 이상하죠? ^^; 원래는 4의 배수로 나와야 하는데 그렇지 않다는 것도 이상하고, 실제로 procexp를 이용해 확인해 봐도,

console_win7_5.png

설령 -1을 취한 값이라고 해도 4, 8, C에 있는 핸들은 이미 다른 목적으로 열려 있는 것들입니다. 잘은 모르겠지만, 아마도 이때까지만 해도 윈도우는 표준 입출력에 대한 핸들을 내부적으로 하드 코딩해 관리했던 것이 아닐까 싶습니다. 그러던 것이, Windows 8/2012 이후부터는 conhost.exe를 개선하면서, 그리고 해당 파일들을 \Device\ConDrv device driver가 관리하면서 (*NIX 계열처럼) 본래의 자리를 차지하게 된 것이 아닐까 추측해 봅니다.




참고로, Command Prompt 창 스스로 process id를 알아내는 재미있는 방법이 있는데요, (리눅스 환경에서는 "echo $$"로 쉽게 알 수 있는 것이지만!)

How to get own process pid from the command prompt in Windows
; https://serverfault.com/questions/126502/how-to-get-own-process-pid-from-the-command-prompt-in-windows

그래서 다음과 같이 실행해 보면,

// Windows 7에서 실행한 경우

c:\temp> title mycmd
c:\temp> tasklist /v /fo csv | findstr /i "mycmd"
"cmd.exe","3736","Console","2","4,392 K","Running","testpc\testusr","0:00:00","mycmd"

위와 같이 cmd.exe의 PID가 3736임을 알 수 있습니다.





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 11/29/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2025-01-22 08시00분
정성태

... 76  77  78  79  [80]  81  82  83  84  85  86  87  88  89  90  ...
NoWriterDateCnt.TitleFile(s)
11936정성태6/10/201918332Math: 58. C# - 최소 자승법의 1차, 2차 수렴 그래프 변화 확인 [2]파일 다운로드1
11935정성태6/9/201919894.NET Framework: 843. C# - PLplot 출력을 파일이 아닌 Window 화면으로 변경
11934정성태6/7/201921228VC++: 133. typedef struct와 타입 전방 선언으로 인한 C2371 오류파일 다운로드1
11933정성태6/7/201919574VC++: 132. enum 정의를 C++11의 enum class로 바꿀 때 유의할 사항파일 다운로드1
11932정성태6/7/201918745오류 유형: 544. C++ - fatal error C1017: invalid integer constant expression파일 다운로드1
11931정성태6/6/201919283개발 환경 구성: 441. C# - CairoSharp/GtkSharp 사용을 위한 프로젝트 구성 방법
11930정성태6/5/201919808.NET Framework: 842. .NET Reflection을 대체할 System.Reflection.Metadata 소개 [1]
11929정성태6/5/201919368.NET Framework: 841. Windows Forms/C# - 클립보드에 RTF 텍스트를 복사 및 확인하는 방법 [1]
11928정성태6/5/201918149오류 유형: 543. PowerShell 확장 설치 시 "Catalog file '[...].cat' is not found in the contents of the module" 오류 발생
11927정성태6/5/201919346스크립트: 15. PowerShell ISE의 스크립트를 복사 후 PPT/Word에 붙여 넣으면 한글이 깨지는 문제 [1]
11926정성태6/4/201919915오류 유형: 542. Visual Studio - pointer to incomplete class type is not allowed
11925정성태6/4/201919736VC++: 131. Visual C++ - uuid 확장 속성과 __uuidof 확장 연산자파일 다운로드1
11924정성태5/30/201921360Math: 57. C# - 해석학적 방법을 이용한 최소 자승법 [1]파일 다운로드1
11923정성태5/30/201921001Math: 56. C# - 그래프 그리기로 알아보는 경사 하강법의 최소/최댓값 구하기파일 다운로드1
11922정성태5/29/201918517.NET Framework: 840. ML.NET 데이터 정규화파일 다운로드1
11921정성태5/28/201924374Math: 55. C# - 다항식을 위한 최소 자승법(Least Squares Method)파일 다운로드1
11920정성태5/28/201916042.NET Framework: 839. C# - PLplot 색상 제어
11919정성태5/27/201920291Math: 54. C# - 최소 자승법의 1차 함수에 대한 매개변수를 단순 for 문으로 구하는 방법 [1]파일 다운로드1
11918정성태5/25/201921135Math: 53. C# - 행렬식을 이용한 최소 자승법(LSM: Least Square Method)파일 다운로드1
11917정성태5/24/201922113Math: 52. MathNet을 이용한 간단한 통계 정보 처리 - 분산/표준편차파일 다운로드1
11916정성태5/24/201919932Math: 51. MathNET + OxyPlot을 이용한 간단한 통계 정보 처리 - Histogram파일 다운로드1
11915정성태5/24/201923055Linux: 11. 리눅스의 환경 변수 관련 함수 정리 - putenv, setenv, unsetenv
11914정성태5/24/201922012Linux: 10. 윈도우의 GetTickCount와 리눅스의 clock_gettime파일 다운로드1
11913정성태5/23/201918754.NET Framework: 838. C# - 숫자형 타입의 bit(2진) 문자열, 16진수 문자열 구하는 방법파일 다운로드1
11912정성태5/23/201918709VS.NET IDE: 137. Visual Studio 2019 버전 16.1부터 리눅스 C/C++ 프로젝트에 추가된 WSL 지원
11911정성태5/23/201917482VS.NET IDE: 136. Visual Studio 2019 - 리눅스 C/C++ 프로젝트에 인텔리센스가 동작하지 않는 경우
... 76  77  78  79  [80]  81  82  83  84  85  86  87  88  89  90  ...