C++
- 有关bare bones(译者注,本Wiki推荐的一个入门教程)的C++内核的快速教程,请参见 Bare Bones
内核可以用C++编程,它与在C中制作内核非常相似,除了你必须考虑一些陷阱 (运行时支持,构造函数等)。 这个页面不会列出这种方法的任何 (劣势) 优势,而是列出了你需要做什么来解决这些事情。
C++提供的许多功能都可以即时使用; 它们不需要额外的支持或代码即可正确使用它们 (例如模板,类,继承,虚拟函数)。 但是,C++的其他部分确实需要运行时支持,本文将专门在这里进行讨论。
简介
如果你已经创建了一个C++内核,如 Bare Bones 文章中所记录的那样,那么许多C++功能已经可用了,就不存在这些问题。 但是,如果你的内核尚未满足ABI(Application Binary Interface 应用程序二进制接口),即使你坚持C和C++的交集,也无法确信编译器不会发出有问题的代码。 特别是,你可能需要初始化进一步的CPU状态以启用浮点寄存器和指令,因为编译器有充分的理由认为浮点寄存器和指令在默认情况可用。
但是,编译器将假定默认情况下所有C++运行时支持都是可用的,但是你没有在libsupc++中链接到C++内核,该内核实现了必要的运行时支持。 这就是为什么你需要加入-fno-rtti和-fno-exception参数到你的交叉编译器,让这些运行时功能是不可用的。 更进一步,你应该将libsupc++链接到你的内核中,但目前已知对于那些开始进行操作系统开发的人来说,它不容易访问,并且GCC构建过程不会对裸elf平台进行正确的交叉编译默认情况下。
你还需要调用 Calling Global Constructors 中记录的全局构造函数,以满足ABI要求,即正确调用程序初始化任务。
纯虚拟函数
如果你想使用纯虚拟函数,你的编译器需要一个单一的支持函数。 仅在无法进行纯虚函数调用的情况下才调用它 (例如,如果你已覆盖对象的虚函数表)。 但是,尽管如此,如果你使用纯虚拟函数并且不提供此支持例程,则你的链接器将抱怨未解决的符号。
在GCC中启用纯虚拟功能相当简单。 你需要做的就是将下面的功能添加到你的C++源文件之一中 (并确保它已链接)。 不必先声明此函数,仅定义对于GCC就足够了。 函数本身甚至不需要做任何事情 (而且在大多数实现中都没有),它只需要 “在那里” 以防万一。
下面你将分别在GCC中找到一个实现示例。
extern "C" void __cxa_pure_virtual()
{
// Do nothing or print an error message.
}
或者,如果你碰巧使用Visual Studio:
int __cdecl _purecall()
{
// Do nothing or print an error message.
}
如果在运行时,你的内核检测到无法调用纯虚拟函数,它将调用上述函数。 这些函数实际上永远不应该被调用,因为如果没有黑客攻击,或者通过内核的未定义行为,就不可能实例化一个没有定义所有纯虚拟函数的类。
全局对象
TODO: 请将此信息与较新的 Calling_Global_Constructors 文章统一起来。
全局对象必须在使用之前调用其构造函数。通常,它们是由启动代码 (你刚刚禁用) 调用的。 所以,为了能够使用它们,你必须为它们编写自己的启动代码。 所有对象都有一个构造函数和一个析构函数。当可执行文件加载到内存中并且程序直接跳到入口点时,将不会调用全局对象的构造函数。 一种解决方案是手动执行此操作。当你的C++入口点被调用时,你可以把这个代码放在第一位:
object1.object1();
object2.object2();
object3.object3();
// ...
全局或静态对象必须由环境构造,然后才能对C++使用。 如果全局/静态对象在其构造函数中需要 “新” 和 “删除”,则应注意。 在这种情况下,最好仅在内核堆准备好使用 (并且你可以访问动态内存分配) 之后才构造这些对象。 不这样做可能会导致对象尝试通过不起作用的 “new” 运算符分配内存。 这也简化了析构函数在 __ cxa_atexit 中的存储,因为你不必使用静态和固定大小的结构。
GCC
注意: 这似乎是安腾平台特有的。对于IA-32/x86/i386和amd64/x86_64,请查看 Calling_Global_Constructors。
根据 Itanium C++ Application Binary Interface (哪个 g' 跟随,而VC不跟随) 函数 _cxa_atexit 用于注册一个析构函数,当需要卸载共享库时,应调用该析构函数。 它应该插入一个函数指针,其中最多包含1个随附参数以及要销毁的对象或共享资源的句柄到表中。
在示例实现中 __cxa_atexit, the __atexit_funcs[ATEXIT_MAX_FUNCS] array acts as the table. 这就是为什么 __cxa_atexit function is defined as:
int __cxa_atexit(void (*destructor) (void *), void *arg, void *__dso_handle);
因此,析构函数 函数指针是析构函数的句柄,arg 是它可能采用的单个参数。 最后, __ dso_handle 是DSO (动态共享对象) 的句柄。
所以总结,你需要定义两个函数和一个符号,以便在你的C文件中使用全局对象:
void *__dso_handle;
int __cxa_atexit(void (*destructor) (void *), void *arg, void *dso);
void __cxa_finalize(void *f);
调用对象构造函数后,GCC会自动调用函数
int __cxa_atexit(void (*destructor) (void *), void *arg, void *dso);
此函数应保存所有三个参数,如果成功返回零,则在失败时不为零。 当你的内核退出时,你应该调用 __ cxa_finalize(0)。 根据ABI规范,以0为参数而不是函数的地址 (要被调用并从列表中删除) 调用此会导致列表中的「所有」析构函数被调用并从列表中删除。
由于你将在内核退出后立即从程序集源中调用此函数,因此可以使用以下代码:
; This is NASM source, mind you.
sub esp, 4
mov [esp], dword 0x0
call __cxa_finalize
add esp, 4
以下是经过测试的,工作的,经过充分评论的来源,比以前在此处找到的来源提供了更详细的解释。 它还强调了可以实施哪些改进以及可以将其插入何处。 要使用它,只需在C++内核源的任何 “一个” 文件 (最好是内核主语句开始的文件) 中包含 icxxabi.h。
File: icxxabi.h
#ifndef _ICXXABI_H
#define _ICXXABI_H
#define ATEXIT_MAX_FUNCS 128
#ifdef __cplusplus
extern "C" {
#endif
typedef unsigned uarch_t;
struct atexit_func_entry_t
{
/*
* Each member is at least 4 bytes large. Such that each entry is 12bytes.
* 128 * 12 = 1.5KB exact.
**/
void (*destructor_func)(void *);
void *obj_ptr;
void *dso_handle;
};
int __cxa_atexit(void (*f)(void *), void *objptr, void *dso);
void __cxa_finalize(void *f);
#ifdef __cplusplus
};
#endif
#endif
File: icxxabi.cpp
#include "./icxxabi.h"
#ifdef __cplusplus
extern "C" {
#endif
atexit_func_entry_t __atexit_funcs[ATEXIT_MAX_FUNCS];
uarch_t __atexit_func_count = 0;
void *__dso_handle = 0; //Attention! Optimally, you should remove the '= 0' part and define this in your asm script.
int __cxa_atexit(void (*f)(void *), void *objptr, void *dso)
{
if (__atexit_func_count >= ATEXIT_MAX_FUNCS) {return -1;};
__atexit_funcs[__atexit_func_count].destructor_func = f;
__atexit_funcs[__atexit_func_count].obj_ptr = objptr;
__atexit_funcs[__atexit_func_count].dso_handle = dso;
__atexit_func_count++;
return 0; /*I would prefer if functions returned 1 on success, but the ABI says...*/
};
void __cxa_finalize(void *f)
{
uarch_t i = __atexit_func_count;
if (!f)
{
/*
* According to the Itanium C++ ABI, if __cxa_finalize is called without a
* function ptr, then it means that we should destroy EVERYTHING MUAHAHAHA!!
*
* TODO:
* Note well, however, that deleting a function from here that contains a __dso_handle
* means that one link to a shared object file has been terminated. In other words,
* We should monitor this list (optional, of course), since it tells us how many links to
* an object file exist at runtime in a particular application. This can be used to tell
* when a shared object is no longer in use. It is one of many methods, however.
**/
//You may insert a prinf() here to tell you whether or not the function gets called. Testing
//is CRITICAL!
while (i--)
{
if (__atexit_funcs[i].destructor_func)
{
/* ^^^ That if statement is a safeguard...
* To make sure we don't call any entries that have already been called and unset at runtime.
* Those will contain a value of 0, and calling a function with value 0
* will cause undefined behaviour. Remember that linear address 0,
* in a non-virtual address space (physical) contains the IVT and BDA.
*
* In a virtual environment, the kernel will receive a page fault, and then probably
* map in some trash, or a blank page, or something stupid like that.
* This will result in the processor executing trash, and...we don't want that.
**/
(*__atexit_funcs[i].destructor_func)(__atexit_funcs[i].obj_ptr);
};
};
return;
};
while (i--)
{
/*
* The ABI states that multiple calls to the __cxa_finalize(destructor_func_ptr) function
* should not destroy objects multiple times. Only one call is needed to eliminate multiple
* entries with the same address.
*
* FIXME:
* This presents the obvious problem: all destructors must be stored in the order they
* were placed in the list. I.e: the last initialized object's destructor must be first
* in the list of destructors to be called. But removing a destructor from the list at runtime
* creates holes in the table with unfilled entries.
* Remember that the insertion algorithm in __cxa_atexit simply inserts the next destructor
* at the end of the table. So, we have holes with our current algorithm
* This function should be modified to move all the destructors above the one currently
* being called and removed one place down in the list, so as to cover up the hole.
* Otherwise, whenever a destructor is called and removed, an entire space in the table is wasted.
**/
if (__atexit_funcs[i].destructor_func == f)
{
/*
* Note that in the next line, not every destructor function is a class destructor.
* It is perfectly legal to register a non class destructor function as a simple cleanup
* function to be called on program termination, in which case, it would not NEED an
* object This pointer. A smart programmer may even take advantage of this and register
* a C function in the table with the address of some structure containing data about
* what to clean up on exit.
* In the case of a function that takes no arguments, it will simply be ignore within the
* function itself. No worries.
**/
(*__atexit_funcs[i].destructor_func)(__atexit_funcs[i].obj_ptr);
__atexit_funcs[i].destructor_func = 0;
/*
* Notice that we didn't decrement __atexit_func_count: this is because this algorithm
* requires patching to deal with the FIXME outlined above.
**/
};
};
};
#ifdef __cplusplus
};
#endif
Visual C++
- 正文: Visual Studio
MSDN帮助和C运行时库源中涵盖了运行构造函数和析构函数。 有关更多信息,请参见MSDN上的 # pragma init_seg 。
基本上发生的情况是,基于 init_seg 的值,将指向函数的指针放置在 .CRT $ XIC,$ XIL,$ xi 中。 然后,链接器按照 $ 之后的字母顺序将 .CRT 部分中的所有内容合并在一起。 如果非零,则调用XIA ('xi_a ) 和XIZ (xi_z) 之间的指针。 将 .CRT 部分与 .data 部分合并,以避免完全分开的部分。
C++支持的一个问题是无法在地图文件中读取的可怕的名称修改。 应该设置一个构建脚本,该脚本通过 undname.exe tool, so that names like ??2@YAPAXI@Z (operator new - I think...) and others are readable.
下面你会发现一些示例代码。 如果要初始化任何静态对象,只需调用 runInit(),如果要运行静态对象析构函数,则调用 runTerm()。
typedef void (*_PVFV)(void);
typedef int (*_PIFV)(void);
typedef void (*_PVFI)(int);
#pragma data_seg(".CRT$XIA")
__declspec(allocate(".CRT$XIA")) _PIFV __xi_a[] = {0};
#pragma data_seg(".CRT$XIZ")
__declspec(allocate(".CRT$XIZ")) _PIFV __xi_z[] = {0};
#pragma data_seg(".CRT$XCA")
__declspec(allocate(".CRT$XCA")) _PVFV __xc_a[] = {0};
#pragma data_seg(".CRT$XCZ")
__declspec(allocate(".CRT$XCZ")) _PVFV __xc_z[] = {0};
#pragma data_seg(".CRT$XPA")
__declspec(allocate(".CRT$XPA")) _PVFV __xp_a[] = {0};
#pragma data_seg(".CRT$XPZ")
__declspec(allocate(".CRT$XPZ")) _PVFV __xp_z[] = {0};
#pragma data_seg(".CRT$XTA")
__declspec(allocate(".CRT$XTA")) _PVFV __xt_a[] = {0};
#pragma data_seg(".CRT$XTZ")
__declspec(allocate(".CRT$XTZ")) _PVFV __xt_z[] = {0};
#pragma data_seg()
#pragma comment(linker, "/merge:.CRT=.data")
static _PVFV onexitarray[32];
static _PVFV *onexitbegin, *onexitend;
int __cdecl _purecall()
{
// print error message
}
int __cdecl atexit(_PVFV fn)
{
if (32*4 < ((int)onexitend-(int)onexitbegin)+4)
return 1;
else
*(onexitend++) = fn;
return 0;
}
EXTERN int runInit()
{
// init the __xi_a to __xi_z: __initex(__xi_a, __xi_z);
// init __xc_a to __xc_z
}
static void __init(_PVFV *pfbegin, _PVFV *pfend)
{
while (pfbegin < pfend)
{
if (*pfbegin != 0)
(**pfbegin)();
++pfbegin;
}
}
static int __initex(_PIFV *pfbegin, _PIFV *pfend)
{
int ret = 0;
while (pfbegin < pfend && ret == 0)
{
if (*pfbegin != 0)
ret = (**pfbegin)();
++pfbegin;
}
return ret;
}
EXTERN void runUninit()
{
if (onexitbegin)
{
while (--onexitend >= onexitbegin)
if (*onexitend != 0)
(**onexitend)();
}
__init(__xp_a, __xp_z);
__init(__xt_a, __xt_z);
}
EXTERN int onexitinit()
{
onexitend = onexitbegin = onexitarray;
*onexitbegin = 0;
return 0;
}
#pragma data_seg(".CRT$XIB") // run onexitinit automatically
__declspec(allocate(".CRT$XIB")) static _PIFV pinit = onexitinit;
#pragma data_seg()
局部静态变量 (GCC Only)
当你声明一个局部静态变量时,GCC会在变量的构造函数调用周围放置一个保护。 这确保只有一个线程可以同时调用构造函数来初始化它。
请注意,这些只是使代码编译的存根,你应该自己实现它们。 只需添加带有测试和设置原语的类似互斥锁的防护。
namespace __cxxabiv1
{
/* guard variables */
/* The ABI requires a 64-bit type. */
__extension__ typedef int __guard __attribute__((mode(__DI__)));
extern "C" int __cxa_guard_acquire (__guard *);
extern "C" void __cxa_guard_release (__guard *);
extern "C" void __cxa_guard_abort (__guard *);
extern "C" int __cxa_guard_acquire (__guard *g)
{
return !*(char *)(g);
}
extern "C" void __cxa_guard_release (__guard *g)
{
*(char *)g = 1;
}
extern "C" void __cxa_guard_abort (__guard *)
{
}
}
GCC发出的实际代码调用本地静态变量的构造函数看起来像这样:
static <type> guard;
if (!guard.first_byte)
{
if (__cxa_guard_acquire (&guard))
{
bool flag = false;
try
{
// Do initialization.
__cxa_guard_release (&guard);
flag = true;
// Register variable for destruction at end of program.
}
catch
{
if (!flag)
{
__cxa_guard_abort (&guard);
}
}
}
}
运算符 'new' and 'delete'
在正确使用 new 和 delete 之前,你必须实现某种内存管理。 你还必须实现两个运算符 (包括它们的数组对应物)。new 和 delete 分别分配和删除内存 (很像C中的 malloc 和 free)。 如果你想了解更多关于这个主题的信息,请看看 Memory Management 文章。
每次调用其中一个运算符 new' 、 new[]' 、 delete 或 delete[] 时,编译器都会向它们插入一个调用。 最简单的实现是将它们映射到内核的 malloc 和 free。例如:
#include <stddef.h>
void *operator new(size_t size)
{
return malloc(size);
}
void *operator new[](size_t size)
{
return malloc(size);
}
void operator delete(void *p)
{
free(p);
}
void operator delete[](void *p)
{
free(p);
}
你也可以让 new 使用 calloc (分配和归零)。 这样,新分配的内存将始终归零 (因此,不包含垃圾)。 但是,标准的 “new” 实现不会清除返回的内存。
你可以移植到操作系统的一个简单的malloc实现是 liballoc. 它只需要基本的 Paging (即存储已使用和空闲页面的列表,并具有查找下一个空闲页面的功能) 即可工作。
Placement New
在C++中 (尤其是在OS代码中,可以在固定地址找到结构),在其他地方获得的内存中构造对象可能很有用。 这是通过一种称为 “placement new” 的技术来实现的。 例如,假设你想在地址 0x09fff0000' 中创建一个APIC对象,那么这段代码将使用placement new 来完成以下操作:
void *apic_address = reinterpret_cast<void *>(0x09FFF0000);
APIC *apic = new (apic_address) APIC;
为了使用placement new ,你需要特殊重载范围中定义的新运算符和删除运算符。 幸运的是,所需的定义很简单,可以在头文件中内联 (C标准将它们放在名为 'new' 的标题中)。
inline void *operator new(size_t, void *p) throw() { return p; }
inline void *operator new[](size_t, void *p) throw() { return p; }
inline void operator delete (void *, void *) throw() { };
inline void operator delete[](void *, void *) throw() { };
由于你的内核没有将已分配的内存标记为正在使用的内存,因此上述实现对于分配内存可能是不安全的。 placement new 几乎从未使用过,如果你希望从内存中的指定地址读取对象,通常更容易创建指向该地址的指针。
你永远不会明确调用placement delete (仅出于某些实现细节原因才需要)。 相反,你只需显式调用对象的析构函数。
apic->~APIC();
RTTI (Run-Time Type Information)
RTTI is used for typeid and dynamic_cast. It requires runtime support as well. Disable it with -fno-rtti. A kernel has no access to run-time features, which are most likely operating system-dependent. Note that virtual functions work without RTTI.
= 例外 =
另一个需要运行时支持的功能。 用 -fno-exceptions 禁用它。异常需要代码来展开堆栈,同时寻找适当的异常处理程序来处理异常。 通常,此代码与你的C应用程序链接在一起,但是在独立的内核中,必须手动提供代码。
Standard Library
注意C++标准库 (stdlib) 与C++标准模板库 (STL) 并不相同。 STL是1994年设计的,在很大程度上影响了C++标准库,但它不是ISO C标准的一部分。 但是,C标准库是C ISO规范的一部分,并且是使用 “std:: vector”,“ std::string ”等时使用的库。 警惕滥用STL这个术语,理想情况下,完全避免使用它。任何人使用它几乎肯定意味着C stdlib。
如果不移植stdlib实现,就不能使用stdlib函数或类。 取决于stdlib的许多现有代码都依赖于OS,因此如果要使用stdlib实现,则必须将其移植到OS。
要访问操作系统中的stdlib,你可以执行以下任一操作:
- 编写一些必需的类模板 (std::string,std::list,std:: coout,...) 的实现。
- 将stdlib实现移植到你的操作系统 (例如 STLport)。
许多stdlib类都需要在你的操作系统中实现 “new” 和 “delete”。 文件访问要求你的操作系统支持读取和包装。控制台功能要求你的操作系统已经具有正常工作的控制台I/O。
移植C++ stdlib (如移植 C标准库) 不会自动使你的操作系统能够读取和写入磁盘或直接从键盘获取数据。 这些只是围绕你的os函数的包装器,必须在你的内核中实现。
请注意,将整个stdlib移植到您的内核通常不是一个好主意,尽管移植几个类模板是合理的,如 std::vector 和 std::string 如果你愿意的话。 至于您的用户应用程序: 越多越好!:)
以下是最常用的stdlib实现的列表:
- STDCXX (a.k.a Apache C++ Standard Library, formally Rogue Wave C++ Standard Library)
- Dinkumware C++ Standard Library
- Microsoft C++ Standard Library (closed source)
- libstdc++ (a.k.a. GNU Standard C++ Library)
- STLport
- uSTL
- libc++ (LLVM C++ Standard library)
Full C++ Runtime Support Using libgcc And libsupc++
如果你想要异常,RTTI,新的和完全删除,你应该使用 libgcc 和libsupc。libgcc包含解卷器 (用于例外),而libsupc包含C支持。 libgcc包含解卷器 (用于例外),而libsupc包含C支持。
你可能会遇到libsupc的问题,但是有 替代库。
优化
有 关于优化你应该知道的事情 也会影响C++,因为它是 C 语言的扩展。 即使你不打算在不久的将来使用C++编译器的优化器,你也应该了解它们。