This is a work in progress of my IM2 routines for z88dk and could be used for reference for assembly use.
I use the z88dk ability to auto create a Z80 interrupt table as a desired location and assign functions.
There is some assembly used in creating the interrupt table, here is an example 
/* interrupt.asm */
SECTION interrupt_vectors
; device vector id 0PUBLIC _vid_ctc_0EXTERN _isr_ctc_0_vid_ctc_0:defw   _isr_ctc_0
; device vector id 1PUBLIC _vid_ctc_1EXTERN _isr_ctc_1_vid_ctc_1:defw   _isr_ctc_1
; device vector id 2PUBLIC _vid_ctc_2EXTERN _isr_ctc_2_vid_ctc_2:defw   _isr_ctc_2
; device vector id 3PUBLIC _vid_ctc_3EXTERN _isr_ctc_3_vid_ctc_3:defw   _isr_ctc_3
; device vector id 4PUBLIC _vid_pio_0_aEXTERN _isr_pio_0_a_vid_pio_0_a:defw   _isr_pio_0_a
; device vector id 5PUBLIC _vid_pio_0_bEXTERN _isr_pio_0_b_vid_pio_0_b:defw   _isr_pio_0_b
; skip vectors 4 through 126defs   120Some examples of the code in the C portion, in the compile settings I set the start of the Interrupt table at 0x0100:
// define our interrupt routines to call
extern void isr_ctc_0(void);extern void isr_ctc_1(void);extern void isr_ctc_2(void);extern void isr_ctc_3(void);
extern void isr_pio_0_a(void);extern void isr_pio_0_b(void);
int main(  ){
int ctc0_int_0 = 0; // example of a flag I set when for example CTC-0 sends the interrupt__asm     ld  hl, (0x100)    ld (_add_isr),hl // Just for test to see memory location of first interrupt routine in the table  ld a, 0x01  // Here we set the upper 8-bit address of the interrupt table.     ld i, a  // load Z80 interrupt register i     im 2  // Interrupt mode     ei    // Enable Interrupts 
__endasm;
// CTC low byte interrupt is set by writing to CTC 0
z80_outp( 0x20, 0x00); // You can only set CTC_0 vector for all four channels and lower three bits of reserved so table must always be in order of CTC_0, CTC_1, CTC_2, CTC_3, CTC is at start of table 0x0100, CTC automatically add the channel that has reach count                                            // 0x0100 = CTC_0, 0x0102 = CTC_1, CTC_2 = 0x0104, CTC_3 = 0x0106
}
void isr_ctc_0(void)  // Basic interrupt routine format for C routines called by interrupt{__asm
      push ix      push hl      di      ld  hl,0x0001   ld (_ctc0_int_0),hl // Write a one to my variable flag to show interrupt was called, but you would put your service routine here for other actions.   pop hl      pop ix      ei      reti
 __endasm;        }   
z88dk compile file example and flags:
Build file
#!/bin/bash
export PATH=/home/eric/projects/z88dk/bin:$PATH  // location of z88dkexport ZCCCFG=/home/eric/projects/z88dk/lib/config
zcc +z80 -m -g -s --max-allocs-per-node2000 -SO3 -startup=1 -clib=sdcc_iy interrupt.asm Hyundai_256x128.c z80_pio.c $1.c -o $1 -lm -create-app -pragma-include:simz80.inc --c-code-in-asm  // Compiler settings and files to include
scp $1.bin pi@192.168.7.2:/home/projects/z80_host  // Write file to my Z80 interface to load code into the Z80 RAM
*simz80.inc*  extra compiler flags and settings
#pragma output CRT_ORG_CODE = 0#pragma output CRT_INCLUDE_PREAMBLE = 0#pragma output CRT_ORG_DATA = 0x2000  // start of RAM#pragma output CRT_MODEL = 1#pragma output REGISTER_SP = 0x4000 // Top of RAM#pragma output CRT_STACK_SIZE = 256#pragma output CLIB_MALLOC_HEAP_SIZE = 0#pragma output CLIB_STDIO_HEAP_SIZE = 0#pragma output CRT_ORG_VECTOR_TABLE = -0x100#pragma output CRT_ENABLE_RST = 0x80#pragma output CRT_ENABLE_EIDI = 0x03#pragma output CRT_INTERRUPT_MODE = 0