Gcc 是否可以在不包含标准库的情况下将字符串输出到C中的控制台?

Gcc 是否可以在不包含标准库的情况下将字符串输出到C中的控制台?,gcc,assembly,x86-64,nasm,win64,Gcc,Assembly,X86 64,Nasm,Win64,我试图更好地理解汇编和机器代码是如何工作的。 因此,我正在使用gcc编译这个简单的snipet: #include <stdio.h> int main(){ printf("Hello World!"); return 0; } 但这包括默认库。我想在不使用printf的情况下输出helloworld,但在C文件中内联一些程序集,并向gcc添加-nostlib和-nodefaultlibs选项。我该怎么做?我正在使用Windows 10和mingw-w64以及In

我试图更好地理解汇编和机器代码是如何工作的。 因此,我正在使用gcc编译这个简单的snipet:

#include <stdio.h>
int main(){
    printf("Hello World!");
    return 0;
}

但这包括默认库。我想在不使用printf的情况下输出helloworld,但在C文件中内联一些程序集,并向gcc添加-nostlib和-nodefaultlibs选项。我该怎么做?我正在使用Windows 10和mingw-w64以及Intel core i7 6700 HQ笔记本电脑处理器。我可以在windows上使用带gcc的NASM吗?

您可以在linux上使用NASM 32位,方法是将字符串移动到内存中写入标准输出文件并调用SYS\u WRITE

在windows上,这样做比较复杂,没有什么有用的学习经验,因此我建议您设置WSL或linux虚拟机并遵循以下步骤

有关如何执行此操作的教程,请参见以下链接: WSL中不支持32位: 64位:

用于设置WSL的链接:

您可以在NASM 32位linux上通过将字符串移动到内存中写入标准输出文件并调用SYS\u WRITE来完成此操作

在windows上,这样做比较复杂,没有什么有用的学习经验,因此我建议您设置WSL或linux虚拟机并遵循以下步骤

有关如何执行此操作的教程,请参见以下链接: WSL中不支持32位: 64位:

用于设置WSL的链接:
我建议不要使用GCC的内联程序集。很难做到正确。你问我可以在windows上使用NASM和GCC吗?。答案是肯定的,请做!您可以将64位NASM代码链接到Win64对象,然后将其链接到C程序

您必须了解Win64 API。与Linux不同,您不需要直接进行系统调用。您可以调用Windows API,它是系统调用接口的薄包装

为了使用写入控制台,您需要使用一个函数,如获取标准输出的句柄,然后调用一个函数,如向控制台写入ANSI字符串

在编写汇编代码时,您必须了解调用约定。由微软公司记录。本文还对其进行了描述。Microsoft文档中的摘要:

调用约定默认值

默认情况下,x64应用程序二进制接口ABI使用四寄存器快速调用约定。在调用堆栈上分配空间作为被调用方保存这些寄存器的卷影存储。函数调用的参数与用于这些参数的寄存器之间存在严格的一一对应关系。任何不适合8字节或不是1、2、4或8字节的参数都必须通过引用传递。一个参数永远不会分布在多个寄存器中。x87寄存器堆栈未使用,被调用方可以使用它,但必须将其视为跨函数调用的易失性。所有浮点操作都使用16个XMM寄存器完成。整数参数在寄存器RCX、RDX、R8和R9中传递。浮点参数在XMM0L、XMM1L、XMM2L和XMM3L中传递。16字节的参数是通过引用传递的。参数传递在参数传递中有详细描述。除了这些寄存器,RAX、R10、R11、XMM4和XMM5被认为是易失性的。所有其他寄存器都是非易失性的

我的注意:阴影存储是32字节,在进行C或Win64 API函数调用之前,必须在堆栈上分配任何堆栈参数

这是一个NASM程序,它调用函数WriteString函数,该函数将要打印的字符串作为第一个参数,将字符串的长度作为第二个参数。WinMain是Windows控制台程序的默认入口点:

global WinMain                  ; Make the default console entry point globally visible
global WriteString              ; Make function WriteString globally visible          

default rel                     ; Default to RIP relative addressing rather
                                ;     than absolute

; External Win API functions available in kernel32
extern WriteConsoleA
extern GetStdHandle
extern ExitProcess

SHADOW_AREA_SIZE  EQU 32
STD_OUTPUT_HANDLE EQU -11

; Read Only Data section
section .rdata use64
strBrownFox db "The quick brown fox jumps over the lazy dog!"
strBrownFox_len equ $-strBrownFox

; Data section (read/write)
section .data use64

; BSS section (read/write) zero-initialized
section .bss use64
numCharsWritten: resd 1      ; reserve space for one 4-byte dword

; Code section
section .text use64

; Default Windows entry point in 64-bit code
WinMain:
    push rsp                 ; Align stack on 16-byte boundary. 8 bytes were
                             ;     pushed by the CALL that reached us. 8+8=16

    lea rcx, [strBrownFox]   ; Parameter 1 = address of string to print
    mov edx, strBrownFox_len ; Parameter 2 = length of string to print
    call WriteString

    xor ecx, ecx             ; Exit and return 0
    call ExitProcess

WriteString:
    push rbp
    mov rbp, rsp             ; Creating a stack frame is optional
    push rdi                 ; Non volatile register we clobber that has to be saved
    push rsi                 ; Non volatile register we clobber that has to be saved
    sub rsp, 16+SHADOW_AREA_SIZE
                             ; The number of bytes pushed must be a multiple of 8
                             ;     to maintain alignment. That includes RBP, the registers
                             ;     we save and restore, the maximum number of extra
                             ;     parameters needed by all the WinAPI calls we make
                             ;     And the Shadow Area Size. 8+8+8+16+32=72.
                             ;     72 is multiple of 8 so at this point our stack
                             ;     is aligned on a 16 byte boundary. 8 bytes were pushed
                             ;     by the call to reach WriteString.
                             ;     72+8=80 = 80 is evenly divisible by 16 so stack remains
                             ;     properly aligned after the SUB instruction

    mov rdi, rcx             ; Store string address to RDI (Parameter 1 = RCX)
    mov esi, edx             ; Store string length to RSI (Parameter 2 = RDX)

    ; HANDLE WINAPI GetStdHandle(
    ;  _In_ DWORD nStdHandle
    ; );
    mov ecx, STD_OUTPUT_HANDLE
    call GetStdHandle

    ; BOOL WINAPI WriteConsole(
    ;  _In_             HANDLE  hConsoleOutput,
    ;  _In_       const VOID    *lpBuffer,
    ;  _In_             DWORD   nNumberOfCharsToWrite,
    ;  _Out_            LPDWORD lpNumberOfCharsWritten,
    ;  _Reserved_       LPVOID  lpReserved
    ; );

    mov ecx, eax             ; RCX = File Handle for STDOUT.
                             ; GetStdHandle returned handle in EAX

    mov rdx, rdi             ; RDX = address of string to display
    mov r8d, esi             ; R8D = length of string to display       
    lea r9, [numCharsWritten]
    mov qword [rsp+SHADOW_AREA_SIZE+0], 0
                             ; 5th parameter passed on the stack above
                             ;     the 32 byte shadow space. Reserved needs to be 0 
    call WriteConsoleA

    pop rsi                  ; Restore the non volatile registers we clobbered 
    pop rdi
    mov rsp, rbp
    pop rbp
    ret
可以使用以下命令进行组合和链接:

nasm -f win64 myprog.asm -o myprog.obj
gcc -nostartfiles -nostdlib -nodefaultlibs myprog.obj -lkernel32 -lgcc -o myprog.exe
nasm -f win64 myprog.asm -o myprog.obj
gcc -c cfuncs.c -o cfuncs.obj
gcc -nodefaultlibs -nostdlib -nostartfiles myprog.obj cfuncs.obj -lkernel32 -lgcc -o myprog.exe 
运行myprog.exe时,它应显示:

您还可以将C文件编译成对象文件,并将它们链接到此代码,并从程序集中调用它们。在本例中,GCC只是用作链接器

编译C文件并与汇编代码链接 此示例与第一个示例类似,只是我们创建了一个名为cfuncs.C的C文件,该文件调用我们的汇编语言WriteString函数来打印Hello,world!:

cfuncs.c

myprog.asm

要组装、编译和链接到可执行文件,可以使用以下命令:

nasm -f win64 myprog.asm -o myprog.obj
gcc -nostartfiles -nostdlib -nodefaultlibs myprog.obj -lkernel32 -lgcc -o myprog.exe
nasm -f win64 myprog.asm -o myprog.obj
gcc -c cfuncs.c -o cfuncs.obj
gcc -nodefaultlibs -nostdlib -nostartfiles myprog.obj cfuncs.obj -lkernel32 -lgcc -o myprog.exe 
myprog.exe的输出应为:


我建议不要使用GCC的内联程序集。很难做到正确。你问我可以在windows上使用NASM和GCC吗?。答案是肯定的,请做!您可以将64位NASM代码链接到Win64对象,然后将其链接到C程序

您必须了解Win64 API。与Linux不同,您不需要直接进行系统调用。您可以调用Windows API,它是系统调用接口的薄包装

为了使用写入控制台,您需要使用一个函数,例如获取STDOUT的句柄,然后调用funct 我喜欢将ANSI字符串写入控制台

在编写汇编代码时,您必须了解调用约定。由微软公司记录。本文还对其进行了描述。Microsoft文档中的摘要:

调用约定默认值

默认情况下,x64应用程序二进制接口ABI使用四寄存器快速调用约定。在调用堆栈上分配空间作为被调用方保存这些寄存器的卷影存储。函数调用的参数与用于这些参数的寄存器之间存在严格的一一对应关系。任何不适合8字节或不是1、2、4或8字节的参数都必须通过引用传递。一个参数永远不会分布在多个寄存器中。x87寄存器堆栈未使用,被调用方可以使用它,但必须将其视为跨函数调用的易失性。所有浮点操作都使用16个XMM寄存器完成。整数参数在寄存器RCX、RDX、R8和R9中传递。浮点参数在XMM0L、XMM1L、XMM2L和XMM3L中传递。16字节的参数是通过引用传递的。参数传递在参数传递中有详细描述。除了这些寄存器,RAX、R10、R11、XMM4和XMM5被认为是易失性的。所有其他寄存器都是非易失性的

我的注意:影子存储是32个字节,在进行C或Win64 API函数调用之前,必须在任何堆栈参数之后在堆栈上分配这些字节

这是一个NASM程序,它调用函数WriteString函数,该函数将要打印的字符串作为第一个参数,将字符串的长度作为第二个参数。WinMain是Windows控制台程序的默认入口点:

global WinMain                  ; Make the default console entry point globally visible
global WriteString              ; Make function WriteString globally visible          

default rel                     ; Default to RIP relative addressing rather
                                ;     than absolute

; External Win API functions available in kernel32
extern WriteConsoleA
extern GetStdHandle
extern ExitProcess

SHADOW_AREA_SIZE  EQU 32
STD_OUTPUT_HANDLE EQU -11

; Read Only Data section
section .rdata use64
strBrownFox db "The quick brown fox jumps over the lazy dog!"
strBrownFox_len equ $-strBrownFox

; Data section (read/write)
section .data use64

; BSS section (read/write) zero-initialized
section .bss use64
numCharsWritten: resd 1      ; reserve space for one 4-byte dword

; Code section
section .text use64

; Default Windows entry point in 64-bit code
WinMain:
    push rsp                 ; Align stack on 16-byte boundary. 8 bytes were
                             ;     pushed by the CALL that reached us. 8+8=16

    lea rcx, [strBrownFox]   ; Parameter 1 = address of string to print
    mov edx, strBrownFox_len ; Parameter 2 = length of string to print
    call WriteString

    xor ecx, ecx             ; Exit and return 0
    call ExitProcess

WriteString:
    push rbp
    mov rbp, rsp             ; Creating a stack frame is optional
    push rdi                 ; Non volatile register we clobber that has to be saved
    push rsi                 ; Non volatile register we clobber that has to be saved
    sub rsp, 16+SHADOW_AREA_SIZE
                             ; The number of bytes pushed must be a multiple of 8
                             ;     to maintain alignment. That includes RBP, the registers
                             ;     we save and restore, the maximum number of extra
                             ;     parameters needed by all the WinAPI calls we make
                             ;     And the Shadow Area Size. 8+8+8+16+32=72.
                             ;     72 is multiple of 8 so at this point our stack
                             ;     is aligned on a 16 byte boundary. 8 bytes were pushed
                             ;     by the call to reach WriteString.
                             ;     72+8=80 = 80 is evenly divisible by 16 so stack remains
                             ;     properly aligned after the SUB instruction

    mov rdi, rcx             ; Store string address to RDI (Parameter 1 = RCX)
    mov esi, edx             ; Store string length to RSI (Parameter 2 = RDX)

    ; HANDLE WINAPI GetStdHandle(
    ;  _In_ DWORD nStdHandle
    ; );
    mov ecx, STD_OUTPUT_HANDLE
    call GetStdHandle

    ; BOOL WINAPI WriteConsole(
    ;  _In_             HANDLE  hConsoleOutput,
    ;  _In_       const VOID    *lpBuffer,
    ;  _In_             DWORD   nNumberOfCharsToWrite,
    ;  _Out_            LPDWORD lpNumberOfCharsWritten,
    ;  _Reserved_       LPVOID  lpReserved
    ; );

    mov ecx, eax             ; RCX = File Handle for STDOUT.
                             ; GetStdHandle returned handle in EAX

    mov rdx, rdi             ; RDX = address of string to display
    mov r8d, esi             ; R8D = length of string to display       
    lea r9, [numCharsWritten]
    mov qword [rsp+SHADOW_AREA_SIZE+0], 0
                             ; 5th parameter passed on the stack above
                             ;     the 32 byte shadow space. Reserved needs to be 0 
    call WriteConsoleA

    pop rsi                  ; Restore the non volatile registers we clobbered 
    pop rdi
    mov rsp, rbp
    pop rbp
    ret
可以使用以下命令进行组合和链接:

nasm -f win64 myprog.asm -o myprog.obj
gcc -nostartfiles -nostdlib -nodefaultlibs myprog.obj -lkernel32 -lgcc -o myprog.exe
nasm -f win64 myprog.asm -o myprog.obj
gcc -c cfuncs.c -o cfuncs.obj
gcc -nodefaultlibs -nostdlib -nostartfiles myprog.obj cfuncs.obj -lkernel32 -lgcc -o myprog.exe 
运行myprog.exe时,它应显示:

您还可以将C文件编译成对象文件,并将它们链接到此代码,并从程序集中调用它们。在本例中,GCC只是用作链接器

编译C文件并与汇编代码链接 此示例与第一个示例类似,只是我们创建了一个名为cfuncs.C的C文件,该文件调用我们的汇编语言WriteString函数来打印Hello,world!:

cfuncs.c

myprog.asm

要组装、编译和链接到可执行文件,可以使用以下命令:

nasm -f win64 myprog.asm -o myprog.obj
gcc -nostartfiles -nostdlib -nodefaultlibs myprog.obj -lkernel32 -lgcc -o myprog.exe
nasm -f win64 myprog.asm -o myprog.obj
gcc -c cfuncs.c -o cfuncs.obj
gcc -nodefaultlibs -nostdlib -nostartfiles myprog.obj cfuncs.obj -lkernel32 -lgcc -o myprog.exe 
myprog.exe的输出应为:


在Linux上会更容易。。。有一个简单的系统调用接口,除非代码在内核中运行,否则无法直接操作硬件。Userland程序必须满足于进行系统调用。我将在这里回应@JohnBollinger关于操作系统在您和硬件之间的作用。如果你真的想了解一台机器是如何工作在如此低的水平,你可能会考虑得到一个微控制器,你编译的代码直接在硬件上运行,而没有任何OS在你和金属之间。如果你想打印hello world,你可以给自己准备一个小的显示外设,阅读数据表,找出如何连接它并与之通信。是否允许链接Kernel32.lib?从技术上讲,这不是标准库。@MisterFresh:Raspberry Pi运行Linux,它有一个用于系统调用的稳定/有文档记录的ABI,而Windows则只允许使用DLL调用。但显然Windows默认情况下会将一些DLL加载到您的进程中,即使您没有专门链接它们。但是无论如何,是的,直接在Linux上进行系统调用比在Windows上容易,并且stdout的文件描述符号保证为1,因此您不需要GetStdHandle或任何东西。但是RPi是ARM,不是x86,所以它是一种不同的汇编语言!在Linux上会更容易。。。有一个简单的系统调用接口,除非代码在内核中运行,否则无法直接操作硬件。Userland程序必须满足于进行系统调用。我将在这里回应@JohnBollinger关于操作系统在您和硬件之间的作用。如果你真的想了解一台机器是如何工作在如此低的水平,你可能会考虑得到一个微控制器,你编译的代码直接在硬件上运行,而没有任何OS在你和金属之间。如果你想打印hello world,你可以给自己准备一个小的显示外设,阅读数据表,找出如何连接它并与之通信。是否允许链接Kernel32.lib?从技术上讲,这不是标准库。@MisterFresh:Raspberry Pi运行Linux,它有一个用于系统调用的稳定/有文档记录的ABI,而Windows则只允许使用DLL调用。但显然Windows默认情况下会将一些DLL加载到您的进程中,即使您没有专门链接它们。但是无论如何,是的,直接在Linux上进行系统调用比在Windows上容易,并且stdout的文件描述符号保证为1,因此您不需要GetStdHandle或任何东西。但是RPi是ARM,不是x86,所以它是一种不同的汇编语言!我有
WSL,但这不是在虚拟机中运行吗?它似乎将在代码和金属之间添加更多层。也许在raspberry pi上更容易?在x86-64上不能使用INT 80h。这是32位ABI。请参阅@MisterFresh:Windows不支持直接使用任何系统调用。不同的Windows版本可能会更改呼叫号码,并且没有文档记录。因此,您可以通过syscall使用它们,但这只是一个玩具实验,对于您想要分发的代码来说并不安全。WSL不是一个VM,它只是内核中转换系统调用的一个仿真层。虽然我认为我读到新的WSL基本上会在VMI中运行一个真正的Linux内核,但我不会说它没有用处。在汇编代码中使用WinMain并绕过C启动并不太困难。编写本机windows代码与让OP在WSL中执行不同。与Linux系统调用相比,考虑到对齐要求和阴影空间,调用WinAPI有点麻烦?它似乎将在代码和金属之间添加更多层。也许在raspberry pi上更容易?在x86-64上不能使用INT 80h。这是32位ABI。请参阅@MisterFresh:Windows不支持直接使用任何系统调用。不同的Windows版本可能会更改呼叫号码,并且没有文档记录。因此,您可以通过syscall使用它们,但这只是一个玩具实验,对于您想要分发的代码来说并不安全。WSL不是一个VM,它只是内核中转换系统调用的一个仿真层。虽然我认为我读到新的WSL基本上会在VMI中运行一个真正的Linux内核,但我不会说它没有用处。在汇编代码中使用WinMain并绕过C启动并不太困难。编写本机windows代码与让OP在WSL中执行不同。与Linux系统调用相比,考虑到对齐要求和阴影空间,调用Win API有点麻烦。@PeterCordes:实际上我今晚睡了半天,很快就把注释打出来了,它们是不正确的。我也注意到了这个错误。我不仅弄错了,我还误读了这个问题,并在我们发言时对其进行了修改。当我发布答案时,你会明白我的意思。实际上,我打算删除它,直到我修复它。mov r9,numcharsWrite-我会使用堆栈插槽,或者至少使用带默认rel的RIP相对LEA,而不是mov r64,imm64将地址放入寄存器。与mov rcx、strBrownFox、lea rcx相同,[rel strBrownFox]是将静态地址放入寄存器的标准x86-64方式。我也不喜欢第64位;获得汇编时间错误比允许意外地将64位机器代码汇编到32位对象文件要好。@PeterCordes:我没有使用堆栈槽的原因主要是因为我想在其中一个数据段中放置一些东西来显示数据的去向。有时,示例旨在显示不同的功能。我会接受BSS中的条目,因为它很好地证明了这一点。@MisterFresh:有一个明确的学习曲线。了解调用约定,正确对齐堆栈通常会带来很多麻烦,如果您不熟悉Windows上的程序集,则在WinAPI中查找函数以执行所需操作可能会有点麻烦。事实上,满足调用约定、GPR保留、,在64位模式下,每次调用WinABI时手动对齐堆栈非常繁琐。这就是为什么我写了宏指令来隐藏无聊的琐事,使学习曲线更陡峭,构建更容易:@PeterCordes:实际上我今晚半睡半醒,很快就把评论打出来了,但它们是不正确的。我也注意到了这个错误。我不仅弄错了,我还误读了这个问题,并在我们发言时对其进行了修改。当我发布答案时,你会明白我的意思。将RIP至少写入一个默认的插槽r64,而不是将其写入一个寄存器r64。与mov rcx、strBrownFox、lea rcx相同,[rel strBrownFox]是将静态地址放入寄存器的标准x86-64方式。我也不喜欢第64位;获得汇编时间错误比允许意外地将64位机器代码汇编到32位对象文件要好。@PeterCordes:我没有使用堆栈槽的原因主要是因为我想在其中一个数据段中放置一些东西来显示数据的去向。有时,示例旨在显示不同的功能。我会接受BSS中的条目,因为它很好地证明了这一点。@MisterFresh:有一个明确的学习曲线。理解调用约定,获得正确的堆栈对齐通常会带来最大的麻烦,如果您是Windows上的程序集新手,则在WinAPI中查找函数以执行您想要的操作可能会有点麻烦。事实上,满足调用约定、GPR保留、在每次WinABI调用中手动对齐堆栈都是非常重要的 在64位模式下,它非常单调乏味。这就是我编写宏指令的原因,它隐藏了无聊的琐事,使学习曲线更陡峭,构建更容易: