Visual Studio
This page or section refers to its readers or editors using I, my, we or us. It should be edited to be in an encyclopedic tone. |
Visual Studio can be used as an integrated development environment for writing assembly, C and C++ with fully featured F5 debugging. Code can be compiled and debugged using either a GCC_Cross-Compiler toolchain you as you would normally use, or the Microsoft Visual C++ compiler. Due to the fact the standard Visual C++ compiler is only capable of emitting OMF object files, with the linker only producing PE Binaries, if you pursue this avenue you will have to either use a bootloader which understands these formats or convert them to, for example, ELF or flat binary.
Due to the fact the only known way of doing native i686-elf operating system development in Visual Studio relies on third-party non-free software, the majority of this article explores how the native capabilities of Visual Studio can be used for doing operating systems development.
If you are interested in attempting to use the Visual C++ toolchain for operating systems development, Kaushik Srenevasan's blog serves as a good starting point 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 for Visual C++ operating system development 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.
Native i686-elf Development
Pre-requisites
In order to utilize Visual Studio as a development environment for native i686-elf operating system development, you must first install the following pre-requisites
- Visual Studio 2015+
- VisualGDB (see below)
- NASM 2.11.08
- A GCC Cross-Compiler toolchain that contains GDB. This process has been tested with i686-elf-tools (GCC 7.1.0 / Binutils 2.28 / GDB 8.0)
- QEMU 2.8.50.0
- An assembly syntax highlighter, such as AsmHighlighter (though you might have to compile it yourself)
It is strongly recommended you attempt to use the recommend versions of these items before attempting to use newer versions. In particular, issues have been observed when using NASM and QEMU versions newer than the ones listed above (see below).
Install QEMU to C:\Program Files (x86)\qemu, NASM to C:\Program Files (x86)\nasm and extract i686-elf-tools to C:\Program Files (x86)\i686-elf-tools (such that the path to your GCC is C:\Program Files (x86)\i686-elf-tools\bin\i686-elf-gcc.exe)
Unfortunately, in order to do operating system development in Visual Studio you will require the third-party add-in VisualGDB. Unlike all of the other software on this list, VisualGDB is not free. You can download a 30-day trial of VisualGDB from their website to demonstrate the principles outlined in this article and determine how you want to proceed.
The first time you launch Visual Studio after installing VisualGDB you may be prompted to configure the VISUALGDB_DIR environment variable (which you should do) as well as whether you want to utilize their Clang-based IntelliSense engine (you can disable of the options on this prompt). If you are interested in purchasing VisualGDB, it appears you will need the Custom VisualGDB edition.
This process has been tested with VisualGDB 5.1 in Visual Studio 2015. A sample project demonstrating a basic boot sector and kernel that you can compile, debug and step through can be found here.
Configuration
The following outlines how you can configure building and debugging your operating system in Visual Studio from scratch. Rather than configuring these one by one, you can also transplant (and rename based on your project name) the *.vgdbsettings files from the sample project into your solution and you should be good to go.
- In Visual Studio create a new VisualGDB solution using the Custom Project Wizard. If prompted to configure the custom project's settings, click Finish to leave these as their defaults (we will configure them one by one below)
- Right click your project in Solution Explorer and select VisualGDB Project Properties
Build Settings
- On the Build Settings tab, next to Build command: click Customize and fill in the following details
- Command: $(VISUALGDB_DIR)\make.exe
- Arguments: all
- Working directory: $(SourceDir)
- Next to Clean Command: click Customize and fill in the following details
- Command: $(VISUALGDB_DIR)\make.exe
- Arguments: clean
- Working directory: $(SourceDir)
- In the Main binary: field enter the $(BuildDir)\<image> where <image> is the name of the file your Makefile generates that contains your entire operating system. e.g. $(BuildDir)\os-image
- The configuration steps outlined in this tutorial assume that all of your output files will be emitted in the most "convenient" place possible (the same place as your source files, the root of your project, etc). If you are interested in having your output files be emitted to a single directory, you should investigate this after you have the basic configuration working.
- Untick the Try detecting common Makefile types and updating source lists in them option. To begin with we would like to control everything ourselves; you can explore re-enabling options like this once we have the basic configuration working
Debug Settings
- On the Debug settings tab, deselect Break-in to GDB using Ctrl-Break events instead of Ctrl-C (required under Cygwin)
- Next to Use a custom GDB executable: click Customize and fill in the following details
- GDB debugger executable: C:\Program Files (x86)\i686-elf-tools\bin\i686-elf-gdb.exe
- Next to GDB launch command: click Customize and fill in the following details
- Arguments: --interpreter mi --readnow
- Working directory: $(ProjectDir)
- Tick the Use a gdbserver: option then click Customize and fill in the following details
- Command: C:\Program Files (x86)\qemu\qemu-system-i386.exe
- Arguments: -S -gdb tcp::1234,ipv4 -soundhw all -drive file=$(TargetFileName),if=floppy
- Working directory: $(ProjectDir)
- In the Target selection command: field enter -target-select remote :1234
- Change the Debugging start mode: to Use "continue" command
IntelliSense Settings
- On the IntelliSense Settings tab, under Clang IntelliSense set the IntelliSense engine: to Use native Visual Studio IntelliSense engine
GDB Settings
- On the GDB settings tab, untick Support 'step into new instance' through breakpoint in: main
- We don't need a breakpoint in any kind of main function, but if you want one at a later point you can re-enable this and change the function name to the real entrypoint of your kernel.
GDB Startup Commands
- On the GDB startup commands tab, under The following GDB commands will be run AFTER selecting a target: enter the following
symbol-file kernel.elf
add-symbol-file boot_sect.elf 0x7c00
directory $(RemoteSourceDir)/src
- These will load the symbols for your kernel and bootsector respectively, allowing you to debug through the sourcecode in your debugger.
- If you have all of your source under a src/ subdirectory, it appears that NASM may cause all *.asm files but the main one to be resolved using relative, instead of absolute paths, thus resulting in GDB being unable to find them when attempting to set breakpoints. Specifying directory $(RemoteSourceDir)/src adds the src/ folder as an additional search location for GDB to use when resolving breakpoint locations. If you're not using a src/ folder, then you likely don't need this line
If you've successfully completed these steps you should be all ready to start developing your operating system using Visual Studio
Important Considerations
- When you modify an *.asm and recompile, it doesn't seem to automatically detect such files are modified; as such you may need to force rebuild instead, or investigate how to get your make system detect changes to assembly files
- When stepping through assembly code, until you switch to 32-bit protected mode you may have issues with your source files not always lining up with where the debugger is currently at. This seems to be an unavoidable consequence of trying to debug 16-bit code; once you start debugging 32-bit assembly or C/C++ however it all seems to be fine
- When you terminate QEMU, GDB will detect the process was terminated but won't actually end the debug session (as such you'll need to hit the Stop button yourself)
- If you try and create a Makefile in Solution Explorer with no file extension Visual Studio will probably add a .cpp to the end of it again and move it under the Source files filter. You will probably want to remove the extension again and maybe move it under the project root, outside the Source files filter
- Remember that in C++ projects in Visual Studio, folders in Solution Explorer simply represent "filters" rather than actual folders; as such as you start organizing your files remember to place your new files under the actual folder they belong in, along with organizing them under the correct filter
- QEMU versions newer than the stipualted version may experience errors when the recommended version of GDB 8.0 attempts to attach to them. If you wish to use a newer QEMU version, you will need to investigate whether modifying your QEMU command line arguments or compiling a newer GDB version resolves this issue
- NASM versions newer than the stipulated version may not generate symbol files properly, resulting in breakpoints in %include'd assembly files always hitting the last line of the file. If you wish to use a newer NASM version, you will need to investigate whether modifying your NASM command line arguments or compiling a newer GDB version resolves this issue
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.