@Override
	public FidHashQuad hash(Function func) throws MemoryAccessException {

		List<CodeUnit> extent = generator.calculateExtent(func);
		if (extent.size() < shortCodeUnitLimit) {
			return null;						// Not enough code units
		}
		Program program = func.getProgram();
		Memory memory = program.getMemory();
		RelocationTable relocationTable = program.getRelocationTable();

		fullDigest.reset();
		specificDigest.reset();

		int specificCount = 0;
		int callCount = 0;
		int codeUnitIndex = -1;
		Iterator<CodeUnit> codeUnitIterator = extent.iterator();
		CodeUnit codeUnit = null;

		while (codeUnitIterator.hasNext()) {
			codeUnit = codeUnitIterator.next();
			++codeUnitIndex;
			if (codeUnitIndex >= Short.MAX_VALUE - 1) {
				break;
			}
			Address minAddress = codeUnit.getMinAddress();
			Address maxAddress = codeUnit.getMaxAddress();
			int amountToFetch = codeUnit.getLength();
			int actualNumberRead = memory.getBytes(minAddress, buffer, 0, amountToFetch);
			if (codeUnit instanceof Instruction) {
				Instruction instruction = (Instruction) codeUnit;
				boolean skip = false;
				for (InstructionSkipper skipper : skippers) {
					if (skipper.shouldSkip(buffer,amountToFetch)) {
						skip = true;
						codeUnitIndex -= 1;			// Don't count this in the score
						break;
					}
				}
				if (skip) {
					continue;
				}

				if (instruction.getFlowType().isCall()) {
					callCount += 1;
				}
				// this block updates the specific hash with all the operand scalars
				// in the instruction (whereas the full hash has masked all the operands
				// out from the instruction opcodes)
				InstructionPrototype prototype = instruction.getPrototype();
				Mask instructMask = prototype.getInstructionMask();
				for (int ii = 0; ii < instruction.getNumOperands(); ++ii) {
					Mask operandMask = prototype.getOperandValueMask(ii);
					if (operandMask == null) {
						continue;
					}
					Object[] opObjects = instruction.getOpObjects(ii);
					int specificUpdate = (ii + 1) * 7777;	// Order independent subhash (into specificDigest) for 1 operand
					int fullUpdate = specificUpdate; // Order independent subhash (into fullDigest) for 1 operand
					for (Object obj : opObjects) {
						if (obj instanceof Scalar) {
							int operandType = instruction.getOperandType(ii);
							Scalar scalar = (Scalar)obj;
							long val = scalar.getSignedValue();
							if (hasRelocation(operandMask,minAddress,maxAddress,relocationTable)) {
								// If we know scalar is part of an address, do not include
								val = 0xfeeddead;		// Placeholder hash, meaning scalar present but not used
							}
							else if (OperandType.isScalar(operandType)) {	// If whole operand is the scalar
								if (OperandType.isAddress(operandType)) {	// Make sure its not an address
									val = 0xfeeddead;	// Scalar is present but value not used in hash
								}
								else {
									specificCount += 1;	// Count the fact that value is used in hash
								}
							}
							else {										// If not the whole operand
								if ((val >= 256) || (val <= -256)) {	// Make sure scalar is small
									val = 0xfeeddead;	// Scalar is present but value not used in hash
								}
								else {
									specificCount += 1;	// Count the fact that value is used in hash
								}
							}
							// Mix scalar value to get more bit diversity, add in a commutative way
							specificUpdate = specificUpdate + ((int) val + 1234567) * 67999;
							fullUpdate += 0xfeeddead;				// Scalar value is never used in full hash, indicate scalar was present
						}
						else if (obj instanceof Register) {			// Registers get thrown in to all 3 hashes
							Register reg = (Register)obj;
							int val = reg.getOffset();		// Use offset in register space in hash
							val = (val + 7654321) * 98777;	// Different mixing function to distinguish from scalar
							fullUpdate += val;
							specificUpdate += val;
						}
						else if (obj instanceof Address) {
							// Hash in placeholder value for address scalar (value is not used)
							specificUpdate = specificUpdate + (0xfeeddead + 1234567) * 67999;
							fullUpdate += 0xfeeddead;
						}
					}
					fullDigest.update(fullUpdate);
					specificDigest.update(specificUpdate);
				}
				try {
					instructMask.applyMask(buffer, 0, buffer, 0);
				}
				catch (NullPointerException e) {
					// something really bad happened with the instruction prototype not being
					// able to tell what the mask is; make the entire code unit constant because it
					// cannot give us reliable information
					for (int ii = 0; ii < actualNumberRead; ++ii) {
						buffer[ii] = (byte) 0xa5;
					}
				}
				catch (IncompatibleMaskException e) {
					throw new RuntimeException(
						"Internal error - mask exception implies buffer too small", e);
				}
			}
			fullDigest.update(buffer, 0, actualNumberRead);
			specificDigest.update(buffer, 0, actualNumberRead);
		}

		// codeUnitIndex is now a length, not the index of the last element
		++codeUnitIndex;

		if (codeUnitIndex < shortCodeUnitLimit) {
			return null;
		}

		short fullCount = (short) (codeUnitIndex - callCount);
		byte additionalSpecificCount = (byte) Math.min(specificCount, Byte.MAX_VALUE);

		return new FidHashQuadImpl(fullCount, fullDigest.digestLong(),
				additionalSpecificCount, specificDigest.digestLong());
	}
Given a FunctionExtentGenerator and a MonitoredMessageDigest, will combine
both to become a FidHasher.
Ghidra/Features/FunctionID/src/main/java/ghidra/feature/fid/hash/MessageDigestFidHasher.java