Visual Studio

From OSDev.wiki
Revision as of 04:33, 10 September 2018 by osdev>Carver
Jump to navigation Jump to search
This article refers to its readers or editors using I, my, we or us.
It should be edited to be in an encyclopedic tone.

It is possible to use Microsoft Visual C++ to write OS kernel, however keep in mind that all the Visual C++ compiler is able to output is OMF format object files, and the linker can only produce PE Binaries, so you will have to either use a bootloader which understands these formats or convert them to, for example, ELF or flat binary.

See also Kaushik Srenevasan's blog to see how you can "[Write] multiboot PE kernels using Visual C++". This is supplemented with part 2 which explains some of the things not covered in the first part.

MinGW32's objcopy should do the job (parameters go something like this: -x -g -X -S -Obinary kernel.bin). But you don't have to strip any information from PE file, the other option is to set section alignment in memory to 0x200 which is generally equal to the section alignment on disk and do some math:

mov eax, [es:0x3c]          ; PE header pointer in MZ header
mov ecx, [es:eax+0x28]      ; AddressOfEntryPoint in PE header

where es is loaded with base address of your PE file in memory, ecx will be loaded with the entry point relative to base address of PE file. Of course, if this is protected mode with a flat 4GB address space, use a register instead of using ES.

Note: The options and procedures described here are for VS.NET 2003. Similar procedures should work on earlier versions. Consult the MSDN or post to the forum if you have any questions.

Visual C++ vs Visual Studio

Visual C++ refers only to the Microsoft C++ IDE and compiler, where as Visual Studio refers to the entire Microsoft family of compilers and IDEs as a whole. In later version, such as the Visual Studio .NET series, all languages share the same IDE program, but in Visual C++ .NET, compatibility for all other languages are removed completely, except through configuring the IDE to manually use another compiler/assembler.

While the Microsoft compilers are free, Visual Studio is not. However, a free edition of Visual Studio is available, known as Visual Studio 2013 Express is available for download from Microsoft's website, but must be activated (freely via e-mail) to continue use after 30-days.

The Microsoft compiler that is provided with the express edition of Visual C++ 2013 can build either native 32-bit or 64-bit PE files, or .NET assemblies.

Of course, you could disable the compiler and add a custom build event which runs a shell script and invokes Cygwin. If you are using Visual Studio (not Express), it is possible to use the Visual Studio SDK to create a "makefile project" that allows you to use a custom build script (such as invoking the Cygwin tool chain) to compile your code, build your image, and launch the emulator (so you can just press F5 for the whole thing to build and the emulator to start). Compiling Bochs with debugging enabled may allow you to use the Visual Studio debugger (including line by line execution) except this hasn't yet been tested.

Creating the Project:

For the kernel and any drivers, create a Win32 Project and select DLL, empty project. Choose DLL if you want to have a kernel that can export functions using the standard Win32 method. It is relatively simple to use this to export functions for use by device drivers...

Custom C++ Runtime

Since you can't use standard C/C++ runtime in your kernel, you'll need to write some of it's functionality yourself. The following article will help to write your custom Visual C++ runtime

Some basic definitions:

#define EXTERN		extern "C"
#define EXPORT		EXTERN __declspec(dllexport)    // exported from DLL
#define IMPORT		EXTERN __declspec(dllimport)    // imported from DLL
#define NAKED		__declspec(naked)		// no prolog or epilog code added
#define NORETURN	__declspec(noreturn)

// Some examples
EXTERN void SomeFunction(int this, int that);
EXPORT int AnotherFunction(unsigned __int64 bigParam);

// In a .cpp file
EXPORT NAKED int AnotherFunction(unsigned __int64)
{
  __asm
  {
    mov eax, dword ptr [esp+4]
    xor eax, dword ptr [esp+8]
    ret
  }
}

I use these to create functions that end up with reasonably undecorated names like _SomeFunction@8 instead of ?@SomeFunction@YAKK000I@@Z (as a __cdecl normal function would be named...) The macros also allow easy import and export from a DLL.

Compiler Options

Here is the meat of this article. These are the compiler options (right-click project, select properties) that I use for my OS.

General

  • Output Directory: .
Add a post-build step to copy only the real output file to the bin directory. Otherwise VS puts .lib and some linker files there as well.
  • Intermediate Directory: .

C/C++ :: General

  • Additional Include Directories: <set as needed>
  • Debug Information Format: Disabled
At the stage my OS is in, I have no use for PDB files. I am in the process of writing a debugger, though, so in the future this could change.
  • Warning Level: Level 4 (/W4)
  • Detect 64-bit Portability Issues: No
This relies on a special __w64 token in various typedefs. I do not know how to use this with my OS. Just be careful: int and long are still 32-bit if compiling for a x64 target (using VS 2005)

C/C++ :: Optimization

  • Optimization: Minimize Size (/O1)
This is really up to you. For me, space is more important than speed for now, but this can easily be changed. If you are implementing source-level debugging, you might want to disable all optimizations.
  • Global Optimizations: Yes (/Og)
Again, enable only if not using a source-level debugger
  • Favor Size Or Speed: Favor Small Code (/Os)
Set as needed, only if /Og enabled
  • Optimize for Processor: Pentium Pro, II, III (/G6)
Set as needed

C/C++ :: Preprocessor

  • Ignore Standard Include Path: Yes (/X)

C/C++ :: Code Generation

  • Enable String Pooling: Yes (/GF)
Places string literals in a read-only data section. This doesn't mean much for OS code, but enable this ONLY if you do not modify string literals in-place, as this would change it in all instances.
  • Enable Minimal Rebuild: No
This option attempts to analyse header files and only rebuilds sources if what it uses has changed. Can speed up building, but also frequently makes mistakes leading to runtime errors. Also enabling this seems to add 0xCC pad bytes to the EXE, which causes bloating.
  • Enable C++ Exceptions: No
Unless you have an exceptional (pun intended) configuration, these require runtime support and are generally not needed anyways.
  • Basic Runtime Checks: Default
Enabling any runtime checks requires special support code.
  • Struct Member Alignment: 1 Byte (/Zp1)
This is really up to you, but most structs that I have need to be aligned this way. If you choose default (8 byte), you can use #pragma pack(push, 1) and #pragma pack(pop) to adjust the packing. Consult MSDN for more info.
  • Buffer Security Check: No
Again, this requires runtime support code

C/C++ (misc. options)

  • Language
  • Force Conformance in For Loop Scope: Yes (/Zc:forScope)
A Good Idea. Makes the i in for (int i = 0; ...) local to the loop.
  • Output Files
  • Assembler Output: Assembly, Machine Code, and Source (/FAcs)
Outputs the assembly listing of the code to files in a given directory. This is nice for assembly-level debugging, as it has the source code lines nearby.
  • ASM List Location: <directory>\
Make sure there is a terminating \, otherwise VS will try to put everything in one file.
  • Advanced
  • Calling Convention: __stdcall (/Gz)
Again, up to you, but I find the lack of name decoration handy for debugging. Functions declared extern "C" void [[Do Something]](int p1, int p2) show up as _[[Do Something]]@8 rather than ?@[[Do Something]]@YAXZSASD or similar.
  • Command Line
  • /Oy-
Disables frame pointer (EBP) omission, included with optimization for size. This is handy to get stack backtraces in case of a crash.

Linker

  • General
  • Output File: <set as needed>
  • Enable Incremental Linking: No (/INCREMENTAL:NO)
Reduces the bloat of the generated EXE or DLL. Linking seems fast enough, anyways.
  • Additional Library Directories: <set as needed>
  • Input
  • Ignore All Default Libraries: Yes (/NODEFAULTLIB)
Ignores the default libc.lib, libcmt.lib, etc.
  • Debugging
  • Generate Debug Info: No
Until I create a better debugger for my OS, I have no use for this. Set as needed.
  • Generate Map File: Yes (/MAP)
Generates a map file (function name and address) and actually sorts by ascending address, unlike GCC.
  • Map File Name: <set as needed>
  • Optimization
  • References: Eliminate Unreferenced Data
  • Enable COMDAT Folding: Remove Redundant COMDATs
  • Advanced
  • Entry Point: <set as needed>
The linker will complain if it is not __stdcall with 12 bytes of arguments (3 32-bit params), but is only a warning.
  • Fixed Base Address: Generate a relocation section
Images will be relocated later. See below.
  • Command Line
  • /DRIVER
  • /ALIGN: 512
Together, they set the Section Alignment and File Alignment to 512 bytes. My boot loader is not sophisticated enough to handle these being different. The downside is that restrictions (read-only, etc) on sections are meaningless, as they require page-granularity for hopefully obvious reasons.

Bootloader Stuff

As part of the build process, I use a tool I wrote to rebase all the PE files. The Microsoft rebase utility (and imagehlp api) only works on a 64K granularity, but I want one with page granularity.

My bootsector is aware of the FAT file system and loads the rest of the bootloader off of the floppy disk. This bootloader reads a configuration file, loads the kernel to 0xC0000000, and then loads each driver on consecutive page-aligned addresses after the kernel. It passes an array of these addresses (and filenames) to the kernel, which can then link the images to itself and call their entry points.

Another option is to use a separate linker such as WLink with a linker script such as the one found on the Watcom page.

Multiboot

To be booted by GRUB, you can make your kernel multiboot. THis involves the embedding of a multiboot header in the first 8K of the image. This can be done as follows:

//Entry point goes here

//The good ol' multiboot header
#pragma pack(push,1)
struct MULTIBOOT_HEADER {
    uint32_t magic;
    uint32_t flags;
    uint32_t checksum;
    uint32_t header_addr;
    uint32_t load_addr;
    uint32_t load_end_addr;
    uint32_t bss_end_addr;
    uint32_t entry_addr;   
};
#pragma pack(pop)

#pragma code_seg(".a")
__declspec(align(4)) MULTIBOOT_HEADER header {
    0x1BADB002,
    0x10003,
    -(0x1BADB002+0x10003),
    (uint32_t)&header - BASE_ADDR + LOADBASE,
    LOAD_BASE,
    0,
    0,
    (uint32_t)&entry - BASE_ADDR + LOADBASE
};

The Rebase Utility

Attached to this thread is the source code for a page-granular rebase utility. It changes the base address of any PE file with a relocation section to the nearest page-aligned address and then removes the relocation section. It compiles under Visual Studio .NET 2003 with default settings successfully, but should work fine on any Microsoft compiler.

To use it, create a text file that contains the relative paths and names of the PE files you wish to rebase, such as

system\kernel.sys
driver\fat.sys
driver\fd.sys
driver\kbdmouse.sys
driver\vga.sys

and call the utility from a batch file or the console as

mvrebase 0xC0000000 rebasefiles.txt

and, in the example given above, it will base kernel.sys at 0xC0000000 and will base subsequent files at the next page-aligned address.

Express 64 bit compilers

You can gain 64 bit compilers by installing the Windows SDK VC++ compilers (please note for VC10 SP1 you need to install the update)

Quirks

In the 64 bit compilers you cannot:

  • Create naked functions
  • Do inline assembly - but it does come with MASM, though you need to separate your C/C++ and assembly into separate source files.
  • Do non-fastcall calling

This is why if you intend to do 64 bit development in MSVC++ you should have an external assembly layer (seperate versions for 32 bit and 64 bit), or use somewhat more limited intrinsics, and for asm if you want to avoid name decoration you need to declare them in a header file like this:

#ifdef __cplusplus //if this is C++
extern "C" { //declare as C functions
#endif
 disable(); //a useful example. disables interrupts (duh!)
 //your functions go here
#ifdef __cplusplus
} //and if it was C++ we need to close the brackets
#endif

And in your asm layer:

BITS 32 ;32 bit version
@disable@0:
cli
ret 
;fastcall name decoration (@0 to be replaced by size of args (bytes)
;Number of bytes is always prefixed by @
BITS 64 ;64 bit version
disable:
cli
ret  ;No decoration at all

Intrinsics

For intrinsics, #include <intrin.h>. This IS suitable for a kernel, but don't ignore standard headers. An example follows:

//main body of intrinsics
#include <intrin.h>
//I/O operations
#include <conio.h>

void someFunc()
{
    __enable();  //STI
    char c = __inbyte(0x60); //IN
    unsigned short w = __inpw(0x1F0);
    unsigned int d = __inp(0xCFC);
    __disable();  //CLI
    __halt();     //HLT
}

From more intrinsics, see Compiler Intrinsics Note that the very helpful __setReg, which is reference in some areas of MSDN, is no longer available.

See Also

Forum Posts