You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
699 lines
23 KiB
699 lines
23 KiB
title "Compute Checksum"
|
|
|
|
;/*++
|
|
;
|
|
; Copyright (c) Microsoft Corporation. All rights reserved.
|
|
;
|
|
; Module Name:
|
|
;
|
|
; xsum.x86
|
|
;
|
|
; Abstract:
|
|
;
|
|
; This module implements a function to compute the checksum of a buffer.
|
|
;
|
|
; Author:
|
|
;
|
|
; David N. Cutler (davec) 27-Jan-1992
|
|
;
|
|
; Revision History:
|
|
;
|
|
; Who When What
|
|
; -------- -------- ----------------------------------------------
|
|
; mikeab 01-22-94 Pentium optimization
|
|
;
|
|
; Environment:
|
|
;
|
|
; Any mode.
|
|
;
|
|
; Revision History:
|
|
;
|
|
;--*/
|
|
|
|
LOOP_UNROLLING_BITS equ 5
|
|
LOOP_UNROLLING equ (1 SHL LOOP_UNROLLING_BITS)
|
|
|
|
.386
|
|
.model small,c
|
|
|
|
assume cs:FLAT,ds:FLAT,es:FLAT,ss:FLAT
|
|
assume fs:nothing,gs:nothing
|
|
|
|
.xlist
|
|
include callconv.inc
|
|
include ks386.inc
|
|
.list
|
|
|
|
.code
|
|
|
|
;++
|
|
;
|
|
; ULONG
|
|
; tcpxsum(
|
|
; IN ULONG cksum,
|
|
; IN PUCHAR buf,
|
|
; IN ULONG len
|
|
; )
|
|
;
|
|
; Routine Description:
|
|
;
|
|
; This function computes the checksum of the specified buffer.
|
|
;
|
|
; Arguments:
|
|
;
|
|
; cksum - Suppiles the initial checksum value, in 16-bit form,
|
|
; with the high word set to 0.
|
|
;
|
|
; buf - Supplies a pointer to the buffer to the checksum buffer.
|
|
;
|
|
; len - Supplies the length of the buffer in bytes.
|
|
;
|
|
; Return Value:
|
|
;
|
|
; The computed checksum in 32-bit two-partial-accumulators form, added to
|
|
; the initial checksum, is returned as the function value.
|
|
;
|
|
;--
|
|
|
|
cksum equ 12 ; stack offset to initial checksum
|
|
buf equ 16 ; stack offset to source address
|
|
len equ 20 ; stack offset to length in words
|
|
|
|
to_checksum_last_word:
|
|
jmp checksum_last_word
|
|
|
|
to_checksum_done:
|
|
jmp checksum_done
|
|
|
|
to_checksum_dword_loop_done:
|
|
jmp checksum_dword_loop_done
|
|
|
|
cPublicProc tcpxsum,3
|
|
|
|
; FPO = 0 dwords locals allocated in prolog
|
|
; 3 dword parameters
|
|
; 2 bytes in prolog
|
|
; 2 registers saved
|
|
; 0 EBP is not used
|
|
; 0 frame type = FPO
|
|
.FPO (0,3,2,2,0,0)
|
|
|
|
push ebx ; save nonvolatile register
|
|
push esi ; save nonvolatile register
|
|
|
|
mov ecx,[esp + len] ; get length in bytes
|
|
sub eax,eax ; clear computed checksum
|
|
test ecx,ecx ; any bytes to checksum at all?
|
|
jz short to_checksum_done ; no bytes to checksum
|
|
|
|
;
|
|
; if the checksum buffer is not word aligned, then add the first byte of
|
|
; the buffer to the input checksum.
|
|
;
|
|
|
|
mov esi,[esp + buf] ; get source address
|
|
sub edx,edx ; set up to load word into EDX below
|
|
test esi,1 ; check if buffer word aligned
|
|
jz short checksum_word_aligned ; if zf, buffer word aligned
|
|
mov ah,[esi] ; get first byte (we know we'll have
|
|
; to swap at the end)
|
|
inc esi ; increment buffer address
|
|
dec ecx ; decrement number of bytes
|
|
jz short to_checksum_done ; if zf set, no more bytes
|
|
|
|
;
|
|
; If the buffer is not an even number of of bytes, then initialize
|
|
; the computed checksum with the last byte of the buffer.
|
|
;
|
|
|
|
checksum_word_aligned: ;
|
|
shr ecx,1 ; convert to word count
|
|
jnc short checksum_start ; if nc, even number of bytes
|
|
mov al,[esi+ecx*2] ; initialize the computed checksum
|
|
jz short to_checksum_done ; if zf set, no more bytes
|
|
|
|
;
|
|
; Compute checksum in large blocks of dwords, with one partial word up front if
|
|
; necessary to get dword alignment, and another partial word at the end if
|
|
; needed.
|
|
;
|
|
|
|
;
|
|
; Compute checksum on the leading word, if that's necessary to get dword
|
|
; alignment.
|
|
;
|
|
|
|
checksum_start: ;
|
|
test esi,02h ; check if source dword aligned
|
|
jz short checksum_dword_aligned ; source is already dword aligned
|
|
mov dx,[esi] ; get first word to checksum
|
|
add esi,2 ; update source address
|
|
add eax,edx ; update partial checksum
|
|
; (no carry is possible, because EAX
|
|
; and EDX are both 16-bit values)
|
|
dec ecx ; count off this word (zero case gets
|
|
; picked up below)
|
|
|
|
;
|
|
; Checksum as many words as possible by processing a dword at a time.
|
|
;
|
|
|
|
checksum_dword_aligned:
|
|
push ecx ; so we can tell if there's a trailing
|
|
; word later
|
|
shr ecx,1 ; # of dwords to checksum
|
|
jz short to_checksum_last_word ; no dwords to checksum
|
|
|
|
mov edx,[esi] ; preload the first dword
|
|
add esi,4 ; point to the next dword
|
|
dec ecx ; count off the dword we just loaded
|
|
jz short to_checksum_dword_loop_done
|
|
; skip the loop if that was the only
|
|
; dword
|
|
mov ebx,ecx ; EBX = # of dwords left to checksum
|
|
add ecx,LOOP_UNROLLING-1 ; round up loop count
|
|
shr ecx,LOOP_UNROLLING_BITS ; convert from word count to unrolled
|
|
; loop count
|
|
and ebx,LOOP_UNROLLING-1 ; # of partial dwords to do in first
|
|
; loop
|
|
jz short checksum_dword_loop ; special-case when no partial loop,
|
|
; because fixup below doesn't work
|
|
; in that case (carry flag is
|
|
; cleared at this point, as required
|
|
; at loop entry)
|
|
lea esi,[esi+ebx*4-(LOOP_UNROLLING*4)]
|
|
; adjust buffer pointer back to
|
|
; compensate for hardwired displacement
|
|
; at loop entry point
|
|
; ***doesn't change carry flag***
|
|
jmp loop_entry[ebx*4] ; enter the loop to do the first,
|
|
; partial iteration, after which we can
|
|
; just do 64-word blocks
|
|
; ***doesn't change carry flag***
|
|
|
|
checksum_dword_loop:
|
|
|
|
DEFLAB macro pre,suf
|
|
pre&suf:
|
|
endm
|
|
|
|
TEMP=0
|
|
REPT LOOP_UNROLLING
|
|
deflab loop_entry_,%TEMP
|
|
adc eax,edx
|
|
mov edx,[esi + TEMP]
|
|
TEMP=TEMP+4
|
|
ENDM
|
|
|
|
checksum_dword_loop_end:
|
|
|
|
lea esi,[esi + LOOP_UNROLLING * 4] ; update source address
|
|
; ***doesn't change carry flag***
|
|
dec ecx ; count off unrolled loop iteration
|
|
; ***doesn't change carry flag***
|
|
jnz checksum_dword_loop ; do more blocks
|
|
|
|
checksum_dword_loop_done label proc
|
|
adc eax,edx ; finish dword checksum
|
|
mov edx,0 ; prepare to load trailing word
|
|
adc eax,edx
|
|
|
|
;
|
|
; Compute checksum on the trailing word, if there is one.
|
|
; High word of EDX = 0 at this point
|
|
; Carry flag set iff there's a trailing word to do at this point
|
|
;
|
|
|
|
checksum_last_word label proc ; "proc" so not scoped to function
|
|
pop ecx ; get back word count
|
|
test ecx,1 ; is there a trailing word?
|
|
jz short checksum_done ; no trailing word
|
|
add ax,[esi] ; add in the trailing word
|
|
adc eax,0 ;
|
|
|
|
checksum_done label proc ; "proc" so not scoped to function
|
|
mov ecx,eax ; fold the checksum to 16 bits
|
|
ror ecx,16
|
|
add eax,ecx
|
|
mov ebx,[esp + buf]
|
|
shr eax,16
|
|
test ebx,1 ; check if buffer word aligned
|
|
jz short checksum_combine ; if zf set, buffer word aligned
|
|
ror ax,8 ; byte aligned--swap bytes back
|
|
checksum_combine label proc ; "proc" so not scoped to function
|
|
add ax,word ptr [esp + cksum] ; combine checksums
|
|
pop esi ; restore nonvolatile register
|
|
adc eax,0 ;
|
|
pop ebx ; restore nonvolatile register
|
|
stdRET tcpxsum
|
|
|
|
|
|
REFLAB macro pre,suf
|
|
dd pre&suf
|
|
endm
|
|
|
|
align 4
|
|
loop_entry label dword
|
|
dd 0
|
|
TEMP=LOOP_UNROLLING*4
|
|
REPT LOOP_UNROLLING-1
|
|
TEMP=TEMP-4
|
|
reflab loop_entry_,%TEMP
|
|
ENDM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stdENDP tcpxsum
|
|
|
|
|
|
ifndef NO_XMMI
|
|
|
|
|
|
LOOP_UNROLLING_BITS_XMMI equ 4
|
|
LOOP_UNROLLING_XMMI equ (1 SHL LOOP_UNROLLING_BITS_XMMI)
|
|
|
|
;VRSTEST EQU 0
|
|
ifdef VRSTEST
|
|
;
|
|
; Test tcpxsum_xmmi for correctness.
|
|
tcksum equ 8[ebp] ; stack offset to initial checksum
|
|
tbuf equ 12[ebp] ; stack offset to source address
|
|
tlen equ 16[ebp] ; stack offset to length in words
|
|
align
|
|
cPublicProc tcpxsum_xmmi,3
|
|
;int 3
|
|
push ebp
|
|
mov ebp, esp
|
|
push ebx
|
|
push esi
|
|
mov ebx, offset tcpxsum
|
|
mov esi, offset tcpxsum_xmmi1
|
|
; Get a "random" number
|
|
.586p
|
|
rdtsc
|
|
.386p
|
|
and eax, 10H
|
|
jz old_then_new
|
|
; Swap which routine is called first
|
|
push ebx
|
|
mov ebx, esi
|
|
pop esi
|
|
old_then_new:
|
|
; Call the first routine
|
|
push tlen
|
|
push tbuf
|
|
push tcksum
|
|
call ebx
|
|
; Save the answer
|
|
push eax
|
|
; Call the second routine
|
|
push tlen
|
|
push tbuf
|
|
push tcksum
|
|
call esi
|
|
; Check the answer
|
|
cmp eax, [esp]
|
|
jnz different_xsum
|
|
; Same answer, we are done
|
|
pop eax
|
|
pop esi
|
|
pop ebx
|
|
pop ebp
|
|
stdRET tcpxsum_xmmi
|
|
align
|
|
; Different answers, need to debug the problem
|
|
different_xsum:
|
|
; Get both checksums onto the stack
|
|
push eax
|
|
; ... and bugcheck
|
|
;EXTRNP _KeBugCheck,1,IMPORT
|
|
;stdCall _KeBugCheck, <0>
|
|
again:
|
|
int 3
|
|
jmp again
|
|
stdENDP tcpxsum_xmmi
|
|
endif
|
|
|
|
|
|
;++
|
|
;
|
|
; ULONG
|
|
; tcpxsum_xmmi(
|
|
; IN ULONG cksum,
|
|
; IN PUCHAR buf,
|
|
; IN ULONG len
|
|
; )
|
|
;
|
|
; Routine Description:
|
|
;
|
|
; This function computes the checksum of the specified buffer.
|
|
; It uses Processor's prefetch instruction.
|
|
;
|
|
; Arguments:
|
|
;
|
|
; cksum - Suppiles the initial checksum value, in 16-bit form,
|
|
; with the high word set to 0.
|
|
;
|
|
; buf - Supplies a pointer to the buffer to the checksum buffer.
|
|
;
|
|
; len - Supplies the length of the buffer in bytes.
|
|
;
|
|
; Return Value:
|
|
;
|
|
; The computed checksum in 32-bit two-partial-accumulators form, added to
|
|
; the initial checksum, is returned as the function value.
|
|
;
|
|
;--
|
|
|
|
cksum equ 12 ; stack offset to initial checksum
|
|
buf equ 16 ; stack offset to source address
|
|
len equ 20 ; stack offset to length in words
|
|
|
|
to_checksum_last_word_xmmi:
|
|
jmp checksum_last_word_xmmi
|
|
|
|
to_checksum_done_xmmi:
|
|
jmp checksum_done_xmmi
|
|
|
|
to_checksum_dword_loop_done_xmmi:
|
|
jmp checksum_dword_loop_done_xmmi
|
|
|
|
ifdef VRSTEST
|
|
cPublicProc tcpxsum_xmmi1,3
|
|
else
|
|
cPublicProc tcpxsum_xmmi,3
|
|
endif
|
|
|
|
; FPO = 0 dwords locals allocated in prolog
|
|
; 3 dword parameters
|
|
; 2 bytes in prolog
|
|
; 2 registers saved
|
|
; 0 EBP is not used
|
|
; 0 frame type = FPO
|
|
.FPO (0,3,2,2,0,0)
|
|
|
|
push ebx ; save nonvolatile register
|
|
push esi ; save nonvolatile register
|
|
|
|
mov ecx,[esp + len] ; get length in bytes
|
|
sub eax,eax ; clear computed checksum
|
|
test ecx,ecx ; any bytes to checksum at all?
|
|
jz short to_checksum_done_xmmi ; no bytes to checksum
|
|
|
|
;
|
|
; if the checksum buffer is not word aligned, then add the first byte of
|
|
; the buffer to the input checksum.
|
|
;
|
|
|
|
mov esi,[esp + buf] ; get source address
|
|
sub edx,edx ; set up to load word into EDX below
|
|
test esi,1 ; check if buffer word aligned
|
|
jz short checksum_word_aligned ; if zf, buffer word aligned
|
|
mov ah,[esi] ; get first byte (we know we'll have
|
|
; to swap at the end)
|
|
inc esi ; increment buffer address
|
|
dec ecx ; decrement number of bytes
|
|
jz short to_checksum_done_xmmi ; if zf set, no more bytes
|
|
|
|
;
|
|
; If the buffer is not an even number of of bytes, then initialize
|
|
; the computed checksum with the last byte of the buffer.
|
|
;
|
|
|
|
checksum_word_aligned: ;
|
|
shr ecx,1 ; convert to word count
|
|
jnc short checksum_start ; if nc, even number of bytes
|
|
mov al,[esi+ecx*2] ; initialize the computed checksum
|
|
jz short to_checksum_done_xmmi ; if zf set, no more bytes
|
|
|
|
;
|
|
; Compute checksum in large blocks of dwords, with one partial word up front if
|
|
; necessary to get dword alignment, and another partial word at the end if
|
|
; needed.
|
|
;
|
|
|
|
;
|
|
; Compute checksum on the leading word, if that's necessary to get dword
|
|
; alignment.
|
|
;
|
|
|
|
checksum_start: ;
|
|
test esi,02h ; check if source dword aligned
|
|
jz short checksum_dword_aligned ; source is already dword aligned
|
|
mov dx,[esi] ; get first word to checksum
|
|
add esi,2 ; update source address
|
|
add eax,edx ; update partial checksum
|
|
; (no carry is possible, because EAX
|
|
; and EDX are both 16-bit values)
|
|
dec ecx ; count off this word (zero case gets
|
|
; picked up below)
|
|
|
|
;
|
|
; Checksum as many words as possible by processing a dword at a time.
|
|
;
|
|
|
|
checksum_dword_aligned:
|
|
push ecx ; so we can tell if there's a trailing
|
|
; word later
|
|
shr ecx,1 ; # of dwords to checksum
|
|
jz short to_checksum_last_word_xmmi ; no dwords to checksum
|
|
|
|
mov edx,[esi] ; preload the first dword
|
|
add esi,4 ; point to the next dword
|
|
dec ecx ; count off the dword we just loaded
|
|
jz short to_checksum_dword_loop_done_xmmi
|
|
; skip the loop if that was the only
|
|
; dword
|
|
mov ebx,ecx ; EBX = # of dwords left to checksum
|
|
add ecx,LOOP_UNROLLING_XMMI-1 ; round up loop count
|
|
shr ecx,LOOP_UNROLLING_BITS_XMMI ; convert from word count to unrolled
|
|
; loop count
|
|
and ebx,LOOP_UNROLLING_XMMI-1 ; # of partial dwords to do in first
|
|
; loop
|
|
jz short checksum_dword_loop ; special-case when no partial loop,
|
|
; because fixup below doesn't work
|
|
; in that case (carry flag is
|
|
; cleared at this point, as required
|
|
; at loop entry)
|
|
lea esi,[esi+ebx*4-(LOOP_UNROLLING_XMMI*4)]
|
|
; adjust buffer pointer back to
|
|
; compensate for hardwired displacement
|
|
; at loop entry point
|
|
; ***doesn't change carry flag***
|
|
jmp xmmi_loop_entry[ebx*4] ; enter the loop to do the first,
|
|
; partial iteration, after which we can
|
|
; just do 64-word blocks
|
|
; ***doesn't change carry flag***
|
|
|
|
checksum_dword_loop:
|
|
; prefetch the 32-byte cache line from [esi+0]
|
|
db 0fH
|
|
db 18H
|
|
db 46H
|
|
db 00H
|
|
|
|
; prefetch the 32-byte cache line from [esi+20h]
|
|
db 0fH
|
|
db 18H
|
|
db 46H
|
|
db 20H
|
|
|
|
; prefetch the 32-byte cache line from [esi+40h]
|
|
db 0fH
|
|
db 18H
|
|
db 46H
|
|
db 40H
|
|
|
|
; prefetch the 32-byte cache line from [esi+60h]
|
|
db 0fH
|
|
db 18H
|
|
db 46H
|
|
db 60H
|
|
|
|
DEFLAB macro pre,suf
|
|
pre&suf:
|
|
endm
|
|
|
|
TEMP=0
|
|
REPT LOOP_UNROLLING_XMMI
|
|
deflab xmmi_loop_entry_,%TEMP
|
|
adc eax,edx
|
|
mov edx,[esi + TEMP]
|
|
TEMP=TEMP+4
|
|
ENDM
|
|
|
|
checksum_dword_loop_end:
|
|
|
|
lea esi,[esi + LOOP_UNROLLING_XMMI * 4] ; update source address
|
|
; ***doesn't change carry flag***
|
|
dec ecx ; count off unrolled loop iteration
|
|
; ***doesn't change carry flag***
|
|
jnz checksum_dword_loop ; do more blocks
|
|
|
|
checksum_dword_loop_done_xmmi label proc
|
|
adc eax,edx ; finish dword checksum
|
|
mov edx,0 ; prepare to load trailing word
|
|
adc eax,edx
|
|
|
|
;
|
|
; Compute checksum on the trailing word, if there is one.
|
|
; High word of EDX = 0 at this point
|
|
; Carry flag set iff there's a trailing word to do at this point
|
|
;
|
|
|
|
checksum_last_word_xmmi label proc ; "proc" so not scoped to function
|
|
pop ecx ; get back word count
|
|
test ecx,1 ; is there a trailing word?
|
|
jz short checksum_done_xmmi; no trailing word
|
|
add ax,[esi] ; add in the trailing word
|
|
adc eax,0 ;
|
|
|
|
checksum_done_xmmi label proc ; "proc" so not scoped to function
|
|
mov ecx,eax ; fold the checksum to 16 bits
|
|
ror ecx,16
|
|
add eax,ecx
|
|
mov ebx,[esp + buf]
|
|
shr eax,16
|
|
test ebx,1 ; check if buffer word aligned
|
|
jz short checksum_combine_xmmi ; if zf set, buffer word aligned
|
|
ror ax,8 ; byte aligned--swap bytes back
|
|
checksum_combine_xmmi label proc ; "proc" so not scoped to function
|
|
add ax,word ptr [esp + cksum] ; combine checksums
|
|
pop esi ; restore nonvolatile register
|
|
adc eax,0 ;
|
|
pop ebx ; restore nonvolatile register
|
|
stdRET tcpxsum
|
|
|
|
|
|
REFLAB macro pre,suf
|
|
dd pre&suf
|
|
endm
|
|
|
|
align 4
|
|
xmmi_loop_entry label dword
|
|
dd 0
|
|
TEMP=LOOP_UNROLLING_XMMI*4
|
|
REPT LOOP_UNROLLING_XMMI-1
|
|
TEMP=TEMP-4
|
|
reflab xmmi_loop_entry_,%TEMP
|
|
ENDM
|
|
|
|
ifdef VRSTEST
|
|
stdENDP tcpxsum_xmmi1
|
|
else
|
|
stdENDP tcpxsum_xmmi
|
|
endif
|
|
|
|
|
|
endif ; NO_XMMI
|
|
|
|
|
|
ifndef NO_OLD_FLUSHSLIST
|
|
|
|
;++
|
|
;
|
|
; PSINGLE_LIST_ENTRY
|
|
; FASTCALL
|
|
; InterlockedFlushSList (
|
|
; IN PSINGLE_LIST_ENTRY ListHead
|
|
; )
|
|
;
|
|
; Routine Description:
|
|
;
|
|
; This function removes the entire list from a sequenced singly
|
|
; linked list so that access to the list is synchronized in an MP system.
|
|
; If there are no entries in the list, then a value of NULL is returned.
|
|
; Otherwise, the address of the entry at the top of the list is removed
|
|
; and returned as the function value and the list header is set to point
|
|
; to NULL.
|
|
;
|
|
; N.B. The cmpxchg8b instruction is only supported on some processors.
|
|
; If the host processor does not support this instruction, then
|
|
; then following code is patched to contain a jump to the normal
|
|
; pop entry code which has a compatible calling sequence and data
|
|
; structure.
|
|
;
|
|
; Arguments:
|
|
;
|
|
; (ecx) = ListHead - Supplies a pointer to the sequenced listhead from
|
|
; which the list is to be flushed.
|
|
;
|
|
; Return Value:
|
|
;
|
|
; The address of the entire current list, or NULL if the list is
|
|
; empty.
|
|
;
|
|
;--
|
|
|
|
cPublicProc InterlockedFlushSList, 1
|
|
|
|
|
|
|
|
;
|
|
; Save nonvolatile registers and read the listhead sequence number followed
|
|
; by the listhead next link.
|
|
;
|
|
; N.B. These two dwords MUST be read exactly in this order.
|
|
;
|
|
|
|
|
|
push ecx
|
|
|
|
push ebx ; save nonvolatile registers
|
|
push ebp ;
|
|
mov ecx, [esp+16]
|
|
mov ebp, ecx ; save listhead address
|
|
mov edx, [ebp] + 4 ; get current sequence number
|
|
mov eax, [ebp] + 0 ; get current next link
|
|
|
|
;
|
|
; N.B. The following code is the retry code should the compare
|
|
; part of the compare exchange operation fail
|
|
;
|
|
; If the list is empty, then there is nothing that can be removed.
|
|
;
|
|
|
|
Efls10: or eax, eax ; check if list is empty
|
|
jz short Efls20 ; if z set, list is empty
|
|
mov ecx, 0 ; clear sequence number and depth
|
|
mov ebx, 0 ; clear successor entry pointer
|
|
|
|
.586
|
|
ifndef NT_UP
|
|
|
|
lock cmpxchg8b qword ptr [ebp] ; compare and exchange
|
|
|
|
else
|
|
|
|
cmpxchg8b qword ptr [ebp] ; compare and exchange
|
|
|
|
endif
|
|
.386
|
|
|
|
jnz short Efls10 ; if z clear, exchange failed
|
|
|
|
;
|
|
; Restore nonvolatile registers and return result.
|
|
;
|
|
|
|
|
|
|
|
Efls20: pop ebp ; restore nonvolatile registers
|
|
pop ebx ;
|
|
pop ecx
|
|
|
|
stdRET InterlockedFlushSList
|
|
|
|
stdENDP InterlockedFlushSList
|
|
|
|
|
|
|
|
|
|
endif ; NO_OLD_FLUSHSLIST
|
|
|
|
|
|
|
|
end
|