As the title suggests, I'm trying to replace the existing handler for the Timer interrupt in DOS with one of my own.
After searching far and wide for a variety of solutions, I found some Assembly code which does exactly that, and I have even managed to compile and test it, and saw that it works.
The problem now is that the code I found (see further down) is written for TASM, and I wish to use it with some C code that I'm writing, which I compile with GCC.
I've tried to convert the code into GAS (GNU Assembler) syntax, but I can't seem to get it to work (I mostly experienced crashes of one kind or another during my numerous attempts).
I would very much appreciate it if anyone could enlighten me with a solution (be it a working version of the assembly code that GAS can compile, a way to do the entire thing in C -- the "interrupt" keyword doesn't work, and neither does "attribute ((interrupt))" and the like -- or even a way to bridge between TASM and GCC).
I should also probably mention that the DOS system I'm using is actually an OracleVM VirtualBox Manager running a virtual machine with FreeDOS installed on it, and that the compiler I'm using for C is the GCC that is provided with the DJGPP development environment.
This is the working TASM code I have (taken from http://www.programmersheaven.com/mb/x86_asm/276128/276185/re-redefining-the-timer-interrupt-handler/):
_stack SEGMENT STACK
db 32 DUP ('STACK ')
_stack ENDS
_code SEGMENT PARA 'CODE'
ASSUME CS:_code, SS:_stack
Lstart LABEL NEAR
JMP Linstall
;+---------------------------------------------
;| My New 1Ch INT
;| Print 'random' chars to the first video line
new_Int PROC FAR
DEC BYTE PTR CS:Counter
CLD
PUSH AX
MOV AX, 0B800h
MOV ES,AX ; ES = b800h
MOV DI,000h ; DI = 0000h
MOV AH,CS:Counter ; set foreground and background color
MOV AL,CS:Counter ; set char
MOV CX,80
REP STOSW ; From AX to ES:DI
POP AX
STI
IRET
new_Int ENDP
Counter DB 0Fh
;+-----------------------------------------
;| Store old INT and Install the new one
;|
Linstall LABEL NEAR
old_INT DD 00000000h
MOV AL,01Ch ;+-
MOV AH,35h ;| Save old_INT
INT 21h ;|
MOV WORD PTR [old_INT],BX
MOV WORD PTR [old_INT][2],ES
CLI ;+-
PUSH CS ;| Install
POP DS ;|
LEA DX,new_INT
MOV AL,1Ch
MOV AH,25h
INT 21h
MOV AH,0 ;+-
INT 16H ;| Wait for a keypress
;+-----------------------------------------
;| Disinstall and exit
CLI
PUSH DS
LDS DX,CS:[old_INT] ;+-
MOV AL,1Ch ;| Disinstall int
MOV AH,25h ;|
INT 21h ;|
POP DS
STI
MOV AL,0 ;+-
MOV AH,4Ch ;| Exit
INT 21h ;|
_code ENDS
END Lstart
It fully works on my machine. I start the program and see the entire first line of the console replaced by colorful characters that change all the time.
And this is my attempt to convert the above code into GAS syntax:
.file "ttv2.s"
# Define a variable for "randomizing" characters and colors
.globl _MyVar
.section .bss
_MyVar:
.space 1
.section .text
# Define a variable for storing the address of the current ISR
.globl _OldInt
.section .bss
.p2align 2
_OldInt:
.space 4
.section .text
# Program entry point
.text
.globl start
start:
jmp _Install
# This is the new Interrupt Service Routine that is going to be installed
.globl _NewInt
_NewInt:
movb _MyVar, %al
decb %al # Decrement our variable
movb %al, _MyVar
cld
pushw %ax
movw $0xB800, %ax
movw %ax, %es # ES = 0xB800
movw $0, %di # DI = 0
movb _MyVar, %ah # Set the foreground and background colors
movb _MyVar, %al # Set the charater to be displayed
movw $80, %cx # The screen is 80 characters wide
rep stosw # Start copying from AX to AS:DI
popw %ax
sti
iret
.globl _Install
_Install:
# Save old ISR address
movb $0x1C, %al # Set the code for the Timer interrupt
movb $0x35, %ah # 0x35 is the code for getting the current ISR
int $0x21 # 0x21 is the interrupt fot s/getting ISRs
movw %es, %dx #
shll $16, %edx # Save the address of the
movw %bx, %dx # old interrupt handler
movl %edx, _OldInt #
# Install the new ISR
cli
pushw %cs
popw %ds
lea _NewInt, %dx # Set the address of the ISR we're installing
movb $0x1C, %al # Set the code for the Timer interrupt
movb $0x25, %ah # 0x25 is the code for setting a new ISR
int $0x21 # 0x21 is the interrupt fot s/getting ISRs
# Wait for a key press
movl $0, %eax
int $0x16
.globl _Uninstall
_Uninstall:
cli
pushw %ds
lds %cs:_OldInt, %dx # Install the address of the old ISR
movb $0x1C, %al # Set the code for the Timer interrupt
movb $0x25, %ah # 0x25 is the code for setting a new ISR
int $0x21 # 0x21 is the interrupt fot s/getting ISRs
popw %ds
sti
.globl _End
_End:
# Exit
movb $0, %al
movb $0x4C, %ah # 0x4C is the code for program exit in DOS
int $0x21
.ident "GCC: (GNU) 4.5.2"
I compile my file (called "ttv2.s") with the following commands:
as -o ttv2.o ttv2.s
ld -o ttv2.exe ttv2.o
When I run the resulting EXE file (there are no warnings or errors during the assembly and linkage), the program crashes with the error "Exception 0D in ring 0" (and lots of register values).
The TASM version, however, works without a hitch!
So I'm guessing that there is something wrong either with the way I converted the code, or with the way I'm building the final EXE. Or both.
A bit of additional information, should it help in any way:
If I remove the installation commands (the int $0x21), there is no crash, and the program waits for me to hit a key and then exits.
If I keep the installation commands, but remove the wait-for-key command (the int $0x16), the program exits immediately, and there is no crash.
If I keep the installation commands, and replace the wait-for-key command with an active delay loop (a simple loop of 4 billion iterations), the program crashes the same way it did when the wait-for-key command was in place, but after a couple of seconds, rather than immediately.
In both cases with the crash (with the key press or the delay), the program crashes even if I remove just one of the two installation commands.
Thanks in advance for any and all assistance, and sorry for the lengthy post...
You probably need to specify .code16 so it builds the application for 16-bit real mode.
The fact that you're getting an error about rings means that you are for some reason not in 16-bit real mode (like DOS would be run), but rather are in some form of protected mode. So make sure that 1) you are compiling to 16-bit real mode for your assembly commands (i.e., the binary machine code is 16-bit opcodes, not 32-bit opcodes), and 2) that you are running in a 16-bit real mode setting when you attempt to run your EXE.
Secondly, note that in the TASM version, they have placed the Counter variable in the code segment, and are accessing the Counter via an offset from the current code-segment. You on the other-hand have placed your counter variable _MyVar in the BSS section. Depending on how the linker links your binary executable, that variable may not be accessible from your interrupt ... for instance, it may not be accessible within the 64Kb window of the current data segment when the interrupt is running. Therefore I would mirror what they did in the TASM version, and place your counter variable in the code-segment, and access it from the code-segment
Related
I'm trying to make JonesForth run on a recent MacBook out of the box, just using Mac tools.
I started to convert everything 64 bits and attend to the Mac assembler syntax.
I got things to assemble, but I immediately run into a curious segmentation fault:
/* NEXT macro. */
.macro NEXT
lodsq
jmpq *(%rax)
.endm
...
/* Assembler entry point. */
.text
.globl start
.balign 16
start:
cld
mov %rsp,var_SZ(%rip) // Save the initial data stack pointer in FORTH variable S0.
mov return_stack_top(%rip),%rbp // Initialise the return stack.
//call set_up_data_segment
mov cold_start(%rip),%rsi // Initialise interpreter.
NEXT // Run interpreter!
.const
cold_start: // High-level code without a codeword.
.quad QUIT
QUIT is defined like this via macro defword:
.macro defword
.const_data
.balign 8
.globl name_$3
name_$3 :
.quad $4 // Link
.byte $2+$1 // Flags + length byte
.ascii $0 // The name
.balign 8 // Padding to next four-byte boundary
.globl $3
$3 :
.quad DOCOL // Codeword - the interpreter
// list of word pointers follow
.endm
// QUIT must not return (ie. must not call EXIT).
defword "QUIT",4,,QUIT,name_TELL
.quad RZ,RSPSTORE // R0 RSP!, clear the return stack
.quad INTERPRET // Interpret the next word
.quad BRANCH,-16 // And loop (indefinitely)
...more code
When I run this, I get a segmentation fault the first time in the NEXT macro:
(lldb) run
There is a running process, kill it and restart?: [Y/n] y
Process 83000 exited with status = 9 (0x00000009)
Process 83042 launched: '/Users/klapauciusisgreat/jonesforth64/jonesforth' (x86_64)
Process 83042 stopped
* thread #1, stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame #0: 0x0000000100000698 jonesforth`start + 24
jonesforth`start:
-> 0x100000698 <+24>: jmpq *(%rax)
0x10000069a <+26>: nopw (%rax,%rax)
jonesforth`code_DROP:
0x1000006a0 <+0>: popq %rax
0x1000006a1 <+1>: lodsq (%rsi), %rax
Target 0: (jonesforth) stopped.
rax does point to what I think is the dereferenced address, DOCOL:
(lldb) register read
General Purpose Registers:
rax = 0x0000000100000660 jonesforth`DOCOL
So one mystery is:
Why does RAX point to DOCOL instead of QUIT? My guess is that the instruction was halfway executed and the result of the indirection was stored in rax. What are some good pointers to documentation?
Why the segmentation fault?
I commented out the original segment setup code in the original that called brk to set up a data segment. Another [implementation] also did not call it at all, so I thought I could as well ignore this. Is there any magic on how to set up segment permissions with syscalls in a 64-bit binary on Catalina? The make command is pretty much the standard JonesForth one:
jonesforth: jonesforth.S
gcc -nostdlib -g -static $(BUILD_ID_NONE) -o $# $<
P.S.: Yes, I can get JonesForth to work perfectly in Docker images, but that's besides the point. I really want it to work in 64 bit on Catalina, out of the box.
The original code had something like
mov $cold_start,%rsi
And the Apple assembler complains about not being able to use 32 immediate addressing in 64-bit binaries.
So I tried
mov $cold_start(%rip),%rsi
but that also doesn't work.
So I tried
mov cold_start(%rip),%rsi
which assembles, but of course it dereferences cold start, which is not something I need.
The correct way of doing this is apparently
lea cold_start(%rip),%rsi
This seems to work as intended.
I am trying to make some basic system calls in assembly (x86-64 in NASM on OSX), but have so far been unsuccessful.
The only examples I have seen on the web so far are for reading from stdin or writing to stdout, such as this:
global main
section .text
main:
call write
write:
mov rax, 0x2000004
mov rdi, 1
mov rsi, message
mov rdx, length
syscall
section .data
message: db 'Hello, world!', 0xa
length: equ $ - message
However, when I try to use that same pattern to make another system call, it doesn't work (it's saying Bus error: 10):
global main
section .text
main:
call mkdir
mkdir:
mov rax, 0x2000136 ; mkdir system command number
mov rdi, rax ; point destination to system command
mov rsi, directory ; first argument
mov rdx, 755 ; second argument
syscall
section .data
directory: db 'tmp', 0xa
What is the general structure for calling system commands (on OSX in NASM ideally)?
Basically what it seems like you're supposed to do is find your desired system call in here: http://www.opensource.apple.com/source/xnu/xnu-1504.3.12/bsd/kern/syscalls.master. So the "write" one looks like this:
4 AUE_NULL ALL { user_ssize_t write(int fd, user_addr_t cbuf, user_size_t nbyte); }
That is saying:
system call number: 4
number of arguments: 3 (file descriptor, memory address to string/buffer, length of buffer)
So I was beginning to think the general pattern was this:
rax: system call number
rdi: maybe? point to system call ("destination index"), but why the `1` in the write example?
rsi: first argument to system call ("source index", the string in this case)
rdx: second argument to system call
rcx: third argument (if necessary, but not in the system write case)
So then it's like you could do a direct mapping of any of the system commands. So mkdir:
136 AUE_MKDIR ALL { int mkdir(user_addr_t path, int mode); }
would be translated to:
rax: 0x20000136 ; 136 + 20000000
rdi: i dunno, maybe `rax`?
rsi: directory (first argument)
rdx: 755 (mode, second argument)
But yeah, that doesn't work.
What am I doing wrong? What is the general pattern of how to do this so I can test it out on any of the other system commands in syscalls.master? Can you describe the role the different registers play here too? That would help clarify a lot I think.
I believe OSX is following the standard SYSV ABI calling convention, at least your example certainly looks like that. Arguments go in the registers RDI, RSI, RDX, R10, R8, and R9, in order. System call number goes into RAX.
Let's look at write: int fd, user_addr_t cbuf, user_size_t nbyte
The assembly:
mov rdi, 1 ; fd = 1 = stdout
mov rsi, message ; cbuf
mov rdx, length ; nbyte
Now, for mkdir: user_addr_t path, int mode
Obviously you need to put path into rdi and mode into rsi.
mkdir:
mov rax, 0x2000136 ; mkdir system command number
mov rdi, directory ; first argument
mov rsi, 0x1ED ; second argument, 0x1ED = 755 octal
syscall
ret
Note you need ret and the end of mkdir subroutine, and you also need one so your main doesn't fall through into mkdir. Furthermore, you should probably use lea to load the directory argument, and use RIP-relative addressing, such as lea rdi, [rel directory].
You've got it almost right: You need 0x88 (dec 136) for the syscall number. The syscalls in syscall.master are in decimal. You ended up calling getsid (which is syscall 310).
For arguments, don't use syscalls.master since that gives you the kernel perspective which is a tad skewed (when it comes to argument names). You should use /usr/include/unistd.h for the prototypes, and usr/inclunde/sys/syscall.h for the numbers. syscalls.master comes in handy only in cases where the syscalls aren't exported to these files, and those are cases where the master files says NO_SYSCALL_STUB.
As for the ABI, it's the same as System V AMD64 ABI. http://people.freebsd.org/~obrien/amd64-elf-abi.pdf
You can see the system calls as libsystem does them:
otool -tV /usr/lib/system/libsystem_kernel.dylib | more
# seek to /^_mkdir:
_mkdir:
0000000000012dfc movl $0x2000088, %eax
0000000000012e01 movq %rcx, %r10
0000000000012e04 syscall
0000000000012e06 jae 0x12e0d
0000000000012e08 jmpq cerror_nocancel
0000000000012e0d ret
0000000000012e0e nop
0000000000012e0f nop
All the system calls essentially have the structure:
Arguments by this point have been put in RDI,RSI,... as per above
ABI
The system call # is loaded into EAX. The 0x2 implies POSIX
syscall. 0x1 would be a Mach Trap, 0x3 - arch specific, 0x4 -
Diagnostic syscalls
rcx saved into r10
syscall gets executed
<< kernel portion occurs, wherein execution goes into kernel mode through trap,
and the value of eax is used to i) get to system call table and ii) branch to correct
system call >>
kernel mode returns to user mode, past the syscall instruction
EAX now holds the syscall return value, so
that "jae" means if the syscall return value is >=0 - i.e. ok -
continue to the "ret" and return to the user
if not, jump to
cerror_nocancel which loads the value of errno and returns the -1 to
the user.
The Bus error: 10 error appears to be caused by an incorrect syscall number and no exit syscall.
; nasm -f macho64 mkdir.asm && ld -o mkdir mkdir.o && ./mkdir
%define SYSCALL_MKDIR 0x2000088
%define SYSCALL_EXIT 0x2000001
global start
section .text
start:
call mkdir
call exit
ret
mkdir:
mov rax, SYSCALL_MKDIR
mov rdi, directory
mov rsi, 0x1ED
syscall
exit:
mov rax, SYSCALL_EXIT
mov rdi, 0
syscall
section .data
directory: db 'tmp', 0
Summary of changes to the original code:
Renaming the main symbol to start
Changing the mkdir syscall number from 0x2000136 to 0x2000088
Changing the registry assignments
Changing the 0xa character to 0 in the directory variable (works without but results in an incorrect filename)
NASM
I also had to install version 2.10.09 of nasm:
brew install https://raw.githubusercontent.com/Homebrew/homebrew/c1616860c8697ffed8887cae8088ab39141f0308/Library/Formula/nasm.rb
brew switch nasm 2.10.09
This was due to:
No nacho64 support in /usr/bin/nasm
Latest brew version (2.11.08) results in this error: fatal: No section for index 2 offset 0 found
I am trying to learn assembler and am somewhat confused by the method used by osx with nasm macho32 for passing arguments to functions.
I am following the book 'Assembly Language Step By Step' by Jeff Duntemann and using the internet extensively have altered it to run on osx both 32 and 64 bit.
So to begin with the linux version from the book
section .data ; Section containing initialised data
EatMsg db "Eat at Joe's!",10
EatLen equ $-EatMsg
section .bss ; Section containing uninitialised data
section .text ; Section containing code
global start ; Linker needs this to find the entry point!
start:
nop
mov eax, 4 ; Specify sys_write syscall
mov ebx, 1 ; Specify File Descriptor 1: Standard Output
mov ecx, EatMsg ; Pass offset of the message
mov edx, EatLen ; Pass the length of the message
int 0x80 ; Make syscall to output the text to stdout
mov eax, 1 ; Specify Exit syscall
mov ebx, 0 ; Return a code of zero
int 0x80 ; Make syscall to terminate the program
section .data ; Section containing initialised data
EatMsg db "Eat at Joe's!", 0x0a
EatLen equ $-EatMsg
section .bss ; Section containing uninitialised data
section .text ; Section containing code
global start ; Linker needs this to find the entry point!
Then very similarly the 64 bit version for osx, other than changing the register names, replacing int 80H (which I understand is somewhat archaic) and adding 0x2000000 to the values moved to eax (don't understand this in the slightest) there isn't much to alter.
section .data ; Section containing initialised data
EatMsg db "Eat at Joe's!", 0x0a
EatLen equ $-EatMsg
section .bss ; Section containing uninitialised data
section .text ; Section containing code
global start ; Linker needs this to find the entry point!
start:
mov rax, 0x2000004 ; Specify sys_write syscall
mov rdi, 1 ; Specify File Descriptor 1: Standard Output
mov rsi, EatMsg ; Pass offset of the message
mov rdx, EatLen ; Pass the length of the message
syscall ; Make syscall to output the text to stdout
mov rax, 0x2000001 ; Specify Exit syscall
mov rdi, 0 ; Return a code of zero
syscall ; Make syscall to terminate the program
The 32 Bit mac version on the other hand is quite different. I can see we are pushing the arguments to the stack dword, so my question is (and sorry for the long preamble) what is the difference between the stack that eax is being pushed to and dword and why do we just use the registers and not the stack in the 64 bit version (and linux)?
section .data ; Section containing initialised data
EatMsg db "Eat at Joe's!", 0x0a
EatLen equ $-EatMsg
section .bss ; Section containing uninitialised data
section .text ; Section containing code
global start ; Linker needs this to find the entry point!
start:
mov eax, 0x4 ; Specify sys_write syscall
push dword EatLen ; Pass the length of the message
push dword EatMsg ; Pass offset of the message
push dword 1 ; Specify File Descriptor 1: Standard Output
push eax
int 0x80 ; Make syscall to output the text to stdout
add esp, 16 ; Move back the stack pointer
mov eax, 0x1 ; Specify Exit syscall
push dword 0 ; Return a code of zero
push eax
int 0x80 ; Make syscall to terminate the program
Well, you don't quite understand what is dword. Speaking HLL, it is not a variable, but rather a type. So push doword 1 means that you pushes a double word constant 1 into the stack. There only ONE stack, and both the one and the register eax are pushed in it.
The registers are used in linux because they are much faster, especially on old processors. Linux ABI (which is, as far as i know, a descent of System V ABI) was developed quite a long time ago and often used in systems where performance was critical, when the difference was very significant. OSX intel abi is much younger, afaik, and simplicity of using stack where more important in desktop OSX than the negligible slowdown. In 64-bit processors, more registers where added and hence the where more efficient to use them.
I'm trying to make a very simple boot program, but I'm having problems debugging it. I'm using QEMU and connecting with GDB like this:
(gdb) set architecture i8086
(gdb) target remote localhost:1234
(gdb) break *0x7C00
(gdb) layout asm
(gdb) continue
...which works great, and I can get to the first few instructions of my code. However, as soon as I si or ni at one particular int instruction, this pattern appears in the GDB assembly view.
0x8669 add %al,(%bx,%si) │
0x866b add %al,(%bx,%si) │
0x866d add %al,(%bx,%si) │
<ad nauseam>
This doesn't appear to stop, or return to where I expect it to. Here's my assembly code:
stage1.S
#include "stage1.h"
.text
.code16
.org STAGE1_START
.globl _start
_start:
JMP main
NOP
write:
LODSB
OR %al, %al
JZ write_exit
MOV $0xE, %ah
MOV $0x9, %bx
INT $0x10
JMP write
write_exit:
RET
video_setup:
MOV $0x0, %ah
MOV $0x3, %al
INT $0x10 /* This causes the problem. */
RET
LoadMsg: .asciz "Loading second stage..."
BootDrive: .byte
main:
MOV %dl, STAGE1_ABS_POS(BootDrive)
CALL video_setup
LEA STAGE1_ABS_POS(LoadMsg), %si
CALL write
HLT
/* Fill file to 512 bytes, regardless. */
.fill STAGE1_BOOTLOADER_SIZE - (. - _start)
/* BIOS magic. */
.word STAGE1_BOOTLOADER_SIG
stage1.h
#ifndef STAGE_1_H
#define STAGE_1_H
#define STAGE1_START 0x0
#define STAGE1_BOOTLOADER_OFFSET 0x7C00
#define STAGE1_BOOTLOADER_SIG 0xAA55
#define STAGE1_BOOTLOADER_SIZE 0x1FE
#define STAGE1_ABS_POS(X) (X-_start+STAGE1_BOOTLOADER_OFFSET)
#endif
I know the code doesn't jump somewhere randomly in memory, as the program works as it should when I run it without debugging options in QEMU.
What am I doing wrong? Criticism of my code generally is also much appreciated.
I don't know precisely why, but it turns out that interrupts end up confusing the debugger or QEMU's debug mode (or I'm doing something wrong, but I'm not sure what).
Anyway, to work around this, I simply place breakpoints at the next instruction I wish to debug, then continue before an interrupt instruction.
Thanks for the help, commenters. :-)
When attempting to run the following assembly program:
.globl start
start:
pushq $0x0
movq $0x1, %rax
subq $0x8, %rsp
int $0x80
I am receiving the following errors:
dyld: no writable segment
Trace/BPT trap
Any idea what could be causing this? The analogous program in 32 bit assembly runs fine.
OSX now requires your executable to have a writable data segment with content, so it can relocate and link your code dynamically. Dunno why, maybe security reasons, maybe due to the new RIP register. If you put a .data segment in there (with some bogus content), you'll avoid the "no writable segment" error. IMO this is an ld bug.
Regarding the 64-bit syscall, you can do it 2 ways. GCC-style, which uses the _syscall PROCEDURE from libSystem.dylib, or raw. Raw uses the syscall instruction, not the int 0x80 trap. int 0x80 is an illegal instruction in 64-bit.
The "GCC method" will take care of categorizing the syscall for you, so you can use the same 32-bit numbers found in sys/syscall.h. But if you go raw, you'll have to classify what kind of syscall it is by ORing it with a type id. Here is an example of both. Note that the calling convention is different! (this is NASM syntax because gas annoys me)
; assemble with
; nasm -f macho64 -o syscall64.o syscall64.asm && ld -lc -ldylib1.o -e start -o syscall64 syscall64.o
extern _syscall
global start
[section .text align=16]
start:
; do it gcc-style
mov rdi, 0x4 ; sys_write
mov rsi, 1 ; file descriptor
mov rdx, hello
mov rcx, size
call _syscall ; we're calling a procedure, not trapping.
;now let's do it raw
mov rax, 0x2000001 ; SYS_exit = 1 and is type 2 (bsd call)
mov rdi, 0 ; Exit success = 0
syscall ; faster than int 0x80, and legal!
[section .data align=16]
hello: db "hello 64-bit syscall!", 0x0a
size: equ $-hello
check out http://www.opensource.apple.com/source/xnu/xnu-792.13.8/osfmk/mach/i386/syscall_sw.h for more info on how a syscall is typed.
The system call interface is different between 32 and 64 bits. Firstly, int $80 is replaced by syscall and the system call numbers are different. You will need to look up documentation for a 64-bit version of your system call. Here is an example of what a 64-bit program may look like.