一门语言是如何自我扩展的?

我正在学习c++,我刚刚开始学习一些Qt的能力来编码GUI程序。我问了自己以下问题:

c++以前没有语法能够要求操作系统提供一个窗口或通过网络进行通信的方式(我也不完全理解api,我承认),如何突然获得这样的功能通过c++本身编写的库?这一切对我来说似乎非常循环。你能在这些库中找到哪些c++指令?

我知道这个问题对一个有经验的软件开发人员来说似乎微不足道,但我已经研究了几个小时,没有找到任何直接的回答。这已经到了我无法理解Qt教程的地步,因为库的存在对我来说是不可理解的。

12465 次浏览

关键在于操作系统是否可能公开API以及如何使用该API的详细描述。

操作系统提供了一组带有调用约定的api。 调用约定定义了将参数传递给API的方式、返回结果的方式以及执行实际调用的方式

操作系统和为它们创建代码的编译器可以很好地协同工作,所以您通常不必考虑它,只需使用它。

创建窗口不需要特殊的语法。所需要的只是操作系统提供一个API来创建窗口。这样的API由c++提供语法的简单函数调用组成。

此外,C和c++是所谓的系统编程语言,能够访问任意指针(可能由硬件映射到某些设备)。此外,调用在程序集中定义的函数也相当简单,这允许处理器提供的全部操作。因此,使用C或c++和少量的汇编程序来编写操作系统本身是可能的。

还应该提到Qt是一个不好的例子,因为它使用所谓的元编译器扩展 c++ '语法。然而,这与它调用操作系统提供的api来实际绘制或创建窗口的能力无关。

语言(如c++ 11)在纸上是规范,通常用英语书写。查看最新的c++ 11草案(或从ISO供应商那里购买昂贵的最终的规范)。

你通常使用带有某种语言的计算机实现(原则上你可以在没有任何计算机的情况下运行c++程序,例如使用一群人类奴隶来解释它;那样既不道德又低效)

你的c++实现通常工作在某个操作系统之上,并与它通信(使用一些实现特定的代码,通常在一些系统库中)。通常这种通信是通过系统调用完成的。在系统调用(2)中查找instance,以获得Linux内核上可用的系统调用列表。

从应用程序的角度来看,系统调用是一个基本的机器指令,如x86-64上的SYSENTER,具有一些约定(ABI)。

在我的Linux桌面上,Qt库位于X11客户端库之上,通过X Windows协议与X11服务器Xorg通信。

在Linux上,在可执行文件上使用ldd来查看对库的依赖项的(长)列表。在你正在运行的进程上使用pmap来查看哪些在运行时被“加载”了。顺便说一句,在Linux上,你的应用程序可能只使用免费软件,你可以研究它的源代码(从Qt,到Xlib, libc,…内核)以了解更多正在发生的事情

计算机就像洋葱,它有很多许多层,从纯硬件的内核到最外层的应用层。每一层都将自己的一部分暴露给下一个外层,这样外层就可以使用内层的一些功能。

以Windows为例,操作系统为运行在Windows上的应用程序公开了所谓的WIN32 API。Qt库使用该API为使用Qt的应用程序提供自己的API。你使用Qt, Qt使用WIN32, WIN32使用较低级别的Windows操作系统,等等,直到它成为硬件中的电信号。

你是对的,一般来说,图书馆不能使任何不可能的事情成为可能。

但是库并不一定要用c++来编写才能被c++程序使用。即使它们是用c++编写的,它们也可能在内部使用其他不是用c++编写的库。因此,c++没有提供任何方法来做这件事的事实并不妨碍它被添加,只要在c++之外有一些方法来做这件事。

在相当低的级别上,c++(或C)调用的一些函数将用汇编编写,并且该汇编包含所需的指令,以完成在c++中不可能(或不容易)的操作,例如调用一个系统函数。在这一点上,这个系统调用可以做你的计算机所能做的任何东西,仅仅是因为没有任何东西阻止它。

当你试图在屏幕上画一些东西时,你的代码会调用另一段代码,而这段代码又会调用另一段代码(等等),直到最后出现“系统调用”,这是CPU可以执行的特殊指令。如果编译器支持它们的“intrinsic”(编译器通过将它们转换为CPU可以理解的特殊代码来“特殊地”处理的函数),这些指令可以用汇编编写,也可以用c++编写。它们的工作是告诉操作系统做一些事情。

当一个系统调用发生时,一个函数被调用,该函数调用另一个函数(等等),直到最后显示驱动程序被告知在屏幕上画一些东西。在这一点上,显示驱动程序查看物理内存中的一个特定区域,它实际上是内存,而是一个可以写入好像内存的地址范围。然而,相反,写入该地址范围会导致图形硬件拦截内存写入,并在屏幕上绘制一些东西 写入这个内存区域是可以在c++中编码的,因为在软件方面它只是一个常规的内存访问。只是硬件处理方式不同 这就是它如何工作的基本解释

您的c++程序使用Qt库(也是用c++编写的)。Qt库将使用Windows CreateWindowEx函数(在kernel32.dll中用C编写)。或者在Linux下,它可能使用Xlib(也用C编码),但它也可以发送原始字节,在X协议中表示“请为我创建一个窗口”。

与你的catch-22问题相关的是历史注释,“第一个c++编译器是用c++编写的”,尽管实际上它是一个带有一些c++概念的C编译器,足以编译第一个版本,然后它可以编译自己。

类似地,GCC编译器使用GCC扩展:它首先被编译到一个版本,然后用于重新编译自身。(GCC构建说明)

C和c++有两个属性,允许OP所谈论的所有这些可扩展性。

  1. C和c++可以访问内存
  2. C和c++可以调用非C或c++语言中的汇编代码。

在内核或基本的非保护模式平台中,外围设备(如串口或磁盘驱动器)以与RAM相同的方式映射到内存映射中。内存是一系列的开关,翻转外围设备的开关(比如串口或磁盘驱动器)可以让外围设备做一些有用的事情。

在保护模式操作系统中,当用户希望从用户空间访问内核时(比如写入文件系统或在屏幕上绘制像素),需要进行系统调用。C语言没有指令来进行系统调用,但是C语言可以调用汇编代码来触发正确的系统调用,这就是为什么C代码可以与内核对话。

为了使特定平台的编程更容易,系统调用被包装在更复杂的函数中,这些函数可以在自己的程序中执行一些有用的函数。可以直接调用系统调用(使用汇编程序),但是使用平台提供的一个包装器函数可能更容易。

还有另一层API比系统调用有用得多。以malloc为例。这不仅会调用系统来获取大块的内存,而且还会通过对正在发生的事情进行所有簿记来管理这些内存。

Win32 api用一个公共平台小部件集包装了一些图形功能。Qt更进一步,以跨平台的方式包装了Win32(或X Windows) API。

从根本上讲,C编译器将C代码转换为机器代码,由于计算机被设计为使用机器代码,所以你应该期望C能够完成计算机所能完成的大部分工作。包装器库所做的一切都是为您做繁重的工作,这样您就不必做了。

我认为你缺少的概念是系统调用。每个操作系统都提供了大量的资源和功能,您可以利用它们来做与操作系统相关的底层工作。即使当您调用常规的库函数时,它也可能在幕后进行系统调用。

系统调用是一种利用操作系统强大功能的低级方式,但使用起来可能复杂且麻烦,因此通常被“包装”在api中,这样您就不必直接处理它们。但实际上,几乎所有涉及O/S相关资源的操作都将使用系统调用,包括打印、网络和套接字等。

在windows的情况下,微软windows已经把它的GUI写进了内核,所以有一些系统调用来创建窗口,绘制图形等。在其他操作系统中,GUI可能不是内核的一部分,在这种情况下,据我所知,不会有任何与GUI相关的系统调用,您只能在更低的级别上使用任何可用的低级图形和输入相关调用。

如何c++…突然通过库获得这些功能

使用其他库并没有什么神奇的。库是可以调用的简单的大函数包。

假设你在写一个这样的函数

void addExclamation(std::string &str)
{
str.push_back('!');
}

现在如果你包含了这个文件,你可以写addExclamation(myVeryOwnString);。现在你可能会问,“c++是怎么突然有能力在字符串中添加感叹号的?”答案很简单:你写一个函数来做这件事,然后你调用它。

因此,要回答你关于c++如何通过c++编写的库获得绘制窗口的能力的问题,答案是相同的。其他人写函数来做这些,然后编译它们,并以库的形式提供给你。

其他问题回答了窗口绘图的实际工作方式,但你听起来对库的工作方式感到困惑,所以我想解决你问题中最基本的部分。

首先,我觉得有一点误会

c++以前没有语法能够要求操作系统提供一个窗口或一种通过网络通信的方式,它是如何做到的呢

没有用于操作系统操作的语法。这是语义的问题。

突然通过c++本身编写的库获得这些功能

好吧,操作系统主要是用c编写的。你可以使用共享库(所以,dll)来调用外部代码。此外,操作系统代码可以在系统调用*中断上注册系统例程,你可以使用组装调用它们。共享库通常只是为您进行系统调用,因此您可以避免使用内联汇编。

这里有一个很好的教程:http://www.win.tue.nl/~aeb/linux/lk/lk-4.html
它适用于Linux,但原理是相同的

操作系统如何对显卡、网卡等进行操作?这是一个非常广泛的主题,但大多数情况下你需要访问中断,端口或写入一些数据到特殊的内存区域。由于这些操作是受保护的,所以无论如何都需要通过操作系统调用它们。

好问题。每一个新的C或c++开发人员都有这样的想法。在本文的其余部分中,我假设使用的是标准的x86机器。如果你使用的是微软c++编译器,打开你的记事本并输入这个(命名为Test.c)

int main(int argc, char **argv)
{
return 0
}

现在编译这个文件(使用开发人员命令提示符)cl Test.c /FaTest.asm

现在打开Test。在你的记事本里。你看到的是翻译后的代码——C/ c++被翻译成汇编程序。你明白我的意思了吗?

_main   PROC
push    ebp
mov ebp, esp
xor eax, eax
pop ebp
ret 0
_main   ENDP

C/ c++程序是设计来在金属上运行的。这意味着他们可以访问较低级别的硬件,从而更容易利用硬件的功能。比如说,我要在x86机器上编写一个C库getch()。

根据汇编器的不同,我可以这样输入:

_getch proc
xor AH, AH
int 16h
;AL contains the keycode (AX is already there - so just return)
ret

我用汇编程序运行它并生成一个。obj -命名为getch.obj。

然后我写了一个C程序(我不包括任何东西)

extern char getch();


void main(int, char **)
{
getch();
}

现在将这个文件命名为GetChTest.c。通过传递getch来编译这个文件。obj。(或者单独编译到.obj和LINK GetChTest。Obj和getch。Obj一起生成GetChTest.exe)。

运行GetChTest.exe,你会发现它在等待键盘输入。

C/ c++编程不仅仅是语言。要成为一名优秀的C/ c++程序员,你需要很好地理解它所运行的机器类型。你需要知道内存管理是如何处理的,寄存器是如何结构的,等等。对于常规编程,你可能不需要所有这些信息,但它们会对你有极大的帮助。除了基本的硬件知识,如果你了解编译器是如何工作的,这当然是有帮助的。这可以让你在必要时调整你的代码。这是一个有趣的包!

这两种语言都支持__asm关键字,这意味着你也可以混合汇编语言代码。学习C和c++会让你成为一个更全面的程序员。

不需要总是与汇编程序链接。我提到它是因为我认为这会帮助你们更好地理解。大多数这样的库调用都利用了操作系统提供的系统调用/ api(操作系统负责硬件交互)。

为了提供与其他答案略有不同的观点,我将这样回答。

(免责声明:我只是稍微简化了事情,我给出的情况纯粹是假设的,是作为演示概念的一种手段而写的,而不是100%真实的生活)。

从另一个角度考虑问题,假设您刚刚编写了一个简单的操作系统,具有基本的线程、窗口和内存管理功能。你想要实现一个c++库,让用户用c++编程,做一些事情,比如创建窗口,在窗口上绘图等等。问题是,如何做到这一点。

首先,由于c++编译为机器代码,您需要定义一种使用机器代码与c++进行接口的方法。这就是函数的作用所在,函数接受参数并给出返回值,因此它们提供了在不同代码段之间传输数据的标准方式。它们通过建立一个被称为调用协定的东西来做到这一点。

调用协定声明了参数在内存中的位置和方式,以便函数在执行时可以找到它们。当一个函数被调用时,调用函数将参数放在内存中,然后要求CPU跳转到另一个函数,在跳转回调用它的地方之前,它在那里完成它所做的事情。这意味着被调用的代码绝对可以是任何东西,并且不会改变函数的调用方式。然而,在这种情况下,函数背后的代码将与操作系统相关,并对操作系统的内部状态进行操作。

所以,几个月后,你已经整理好了所有的操作系统功能。你的用户可以调用函数来创建窗口并在上面绘图,他们可以创建线程和所有美妙的东西。但问题是,你的操作系统的功能将不同于Linux或Windows的功能。所以你决定给用户一个标准的界面,这样他们就可以编写可移植的代码。这就是QT的用武之地。

正如您几乎肯定知道的那样,QT有大量有用的类和函数来完成操作系统所做的各种事情,但是在某种程度上,QT似乎独立于底层操作系统。QT提供的类和函数在呈现给用户的方式上是统一的,但是函数背后的代码对于每个操作系统是不同的。例如,QT的QApplication::closeAllWindows()实际上会根据所使用的版本调用每个操作系统的专用窗口关闭函数。在Windows中,它很可能调用CloseWindow(hwnd),而在使用X Window系统的操作系统中,它可能会调用XDestroyWindow(display, Window)。

很明显,操作系统有很多层,所有这些层都必须通过各种各样的接口进行交互。有许多方面我还没有谈到,但要全部解释它们将需要很长时间。如果你对操作系统的内部工作原理有进一步的兴趣,我建议你查看操作系统开发wiki

请记住,许多操作系统选择向C/ c++公开接口的原因是它们可以编译为机器代码,它们允许将汇编指令与它们自己的代码混合在一起,并且它们为程序员提供了很大程度的自由。

同样,这里有很多内容。我想继续解释库如何像.so和.dll文件不需要用C/ c++编写,可以用汇编或其他语言编写,但我觉得如果我再添加更多,我还不如写一整篇文章,尽管我很想这样做,但我没有一个站点来托管它。

我如何看待这个问题,这实际上是一个编译器的问题。

这样看,你用汇编写了一段代码(你可以用任何语言写),它把你新写的语言(你想把z++称为汇编)翻译成汇编,为了简单起见,让我们称它为编译器(它是编译器)。

现在你给了这个编译器一些基本的函数,这样你就可以写int,字符串,数组等等,实际上你给了它足够的能力,所以你可以用z++来写编译器本身。现在你有了一个用z++编写的z++编译器,非常简洁。

更酷的是,现在您可以使用编译器已有的功能为其添加功能,从而通过使用以前的功能扩展z++语言的新功能

举个例子,如果你写了足够多的代码来绘制任何颜色的像素,那么你可以使用z++扩展它来绘制任何你想要的东西。

硬件是允许这一切发生的原因。您可以将图形存储器看作一个大数组(由屏幕上的每个像素组成)。要绘制到屏幕上,您可以使用c++或任何允许直接访问该内存的语言写入该内存。该内存恰好可以被显卡访问或位于显卡上。

在现代系统中,由于各种限制,直接访问图形存储器需要编写驱动程序,因此您可以使用间接方法。库创建一个窗口(实际上只是一个像其他图像一样的图像),然后将该图像写入图形内存,然后GPU将其显示在屏幕上。除了写入特定内存位置的能力之外,该语言不需要添加任何东西,这就是指针的作用。